cswork / libraryOpen practice workspace

Interview guides · Canonical

Canonical Software Engineer Interview Guide

Canonical Software Engineer interview guide: written essay, Thomas GIA, Linux internals, Python/Go/Rust and system design questions with a 7-day plan.

The 60-second brief

Canonical is the company behind Ubuntu. Candidates describe its software as running on public cloud instances, enterprise servers, IoT devices and developer workstations. The engineering areas they list for the role include the Linux kernel, Kubernetes and other container tooling, system packaging (Debian packages and Snaps) and cloud-init. A Software Engineer there writes Python, Go, Rust or C/C++, builds test suites, takes part in code review, and works with upstream open-source communities. The reported work also covers CI, packaging and deployment across platforms, tracking down low-level bugs, performance tuning and getting security fixes into the Ubuntu ecosystem. The team is described as fully remote and spread across the world, with most communication in writing through documentation, email and issue trackers.

This guide is for engineers preparing for a Software Engineer loop at Canonical. It covers the four reported stages, the written essay questions, the reported Linux and systems questions, language-semantics questions (Python, Go, Rust), the four system design questions candidates report, and a story bank for the open-source and leadership questions. It is written for candidates whose target team uses Python, Go, Rust or C/C++, and it pairs each reported question with the technique, trade-offs and edge cases worth rehearsing.

Candidates report 4 rounds over roughly 3-5 weeks: an Asynchronous Assessment, Technical Coding Challenges, Domain-Specific Interviews and an Executive Leadership Review. Some candidates say the whole process can stretch to months.

Focus areas

  • Python
  • Written interviews / essays
  • Linux / Unix

Overview

The reported loop front-loads written work. Before any live conversation, candidates describe a long written questionnaire (academic background, proudest project, leadership, open-source experience) and a timed cognitive test called the Thomas GIA that covers perceptual speed, number speed and accuracy, word meaning and spatial visualization. Preparation for this stage is different from preparation for a coding screen: you write real answers in advance, keep specific stories ready, proofread them, and practise speed-with-accuracy puzzles.

The live technical material reported for this role leans toward fundamentals rather than frameworks. The Linux questions ask for mechanisms: the boot path from firmware to user space, processes versus threads and kernel scheduling, how a shell builds a pipe, virtual memory and the OOM killer, and a safe kernel upgrade with a way back. Candidates also mention fork and exec, troubleshooting a server with high load average but low CPU use, and tools such as strace, lsof and htop. Study these until you can explain the system calls and kernel structures involved, not just the vocabulary.

Language questions are split across Python, Go and Rust depending on the team: decorators, goroutines against OS threads and the Go scheduler, Rust ownership, borrowing and lifetimes, an ISBN check-digit function, and the cost of hash maps and slices in your main language. Reported coding scenarios include a concurrent LRU cache in Go and an asynchronous Python log parser that emits a JSON report. Prepare in the language you will actually be assessed in, and be able to state complexity and memory behaviour precisely.

The four design questions all sit close to infrastructure: a highly available package-manager metadata service, a lightweight monitoring agent for thousands of resource-constrained IoT devices, an API gateway with rate limiting, authentication and dynamic routing, and deployment automation for microservices across hybrid clouds. They are not four variations of one problem, so practise each with its own constraints, and keep written answers structured, since written communication is a theme across the reported stages.

The Canonical interview process

These are the four stages candidates report, in the order they describe them. Little is reported about the final stage, so the preparation for it below is deliberately modest.

Asynchronous Assessment

Candidates describe this stage as written assessments plus cognitive testing, completed asynchronously before any live conversation. The written part is a multi-page questionnaire on academic background, career milestones, open-source history and motivation. The cognitive part is the Thomas GIA, which candidates describe as covering perceptual speed, number speed and accuracy, word meaning and spatial visualization such as spotting mirrored letters. Approach it as two separate jobs: write specific, proofread answers built from real projects, then train for speed without sacrificing accuracy.

What to demonstrate

  • Written communication: clear, structured, precise answers about your own work and background
  • Self-reflection and the ability to give concrete detail about projects, contributions and trade-offs
  • Speed and accuracy on timed cognitive tasks (perceptual, numerical, verbal, spatial)

How to prepare

  • Write full answers to the essay-style questions about why Canonical, your proudest technical achievement, a leadership role, and your open-source experience, using specific projects, decisions and outcomes, then proofread them twice
  • Prepare honest answers about your mathematics and physics background, including what you took and how you performed, before you are asked
  • Run timed practice in each GIA area (number comparison, visual difference spotting, synonyms and antonyms, mirrored shapes) and favour a high accuracy rate over finishing everything

Technical Coding Challenges

Candidates report technical coding challenges at this point, with emphasis on classic data structure and algorithm problems, idiomatic and readable code, and the runtime behaviour of the language you choose. Scenarios candidates mention include a thread-safe LRU cache in Go and an asynchronous Python log parser that extracts error metrics into a JSON report. Pick one primary language among Python, Go and Rust and prepare in it, since the questions about complexity and memory footprint are framed around your primary language.

What to demonstrate

  • Working, idiomatic code in the language you chose
  • Choice of data structures with correct time and space complexity
  • Handling of edge cases and errors, for example in the ISBN check-digit function

How to prepare

  • Implement an LRU cache with a hash map plus doubly linked list, then make it safe for concurrent use and reason about lock granularity and contention
  • Write a log parser that reads a large file lazily, aggregates error counts and emits JSON, and decide where asynchronous I/O helps and where it does not
  • Write the ISBN check-digit validator for both ISBN-10 and ISBN-13 and list your edge cases (hyphens, a trailing X, wrong length, non-digit characters) before you code
  • Prepare a short account of hash-map and slice costs in your language: average and worst-case lookup, amortized growth, resize behaviour and extra memory per entry

Domain-Specific Interviews

Candidates describe live interviews on specific technical domains relevant to the role. The material reported for this stage family is Linux and systems fundamentals (process management, signals, boot sequence, networking and I/O, cgroups and namespaces, profiling tools) and language semantics in Python, Go or Rust. Candidates advise being ready for specific commands, flags, system calls and kernel behaviour rather than high-level architecture talk. Practise explaining mechanisms aloud in order, from cause to effect.

What to demonstrate

  • Depth of Linux internals: boot, processes and threads, pipes, virtual memory, kernel upgrades
  • Understanding of runtime and memory behaviour in Python, Go or Rust
  • Troubleshooting method on a misbehaving system, for example high load average with low CPU utilization

How to prepare

  • Rehearse the boot sequence step by step (firmware, bootloader, kernel and initramfs, systemd, targets) and say what each stage hands to the next
  • Explain fork and exec, copy-on-write, zombies and signals, then contrast threads and processes in terms of shared address space and scheduler entities
  • Build a debugging sequence for high load with low CPU: check for tasks in uninterruptible I/O wait, look at disk and network, then use strace, lsof and per-process views
  • Practise a safe kernel upgrade plan: staged rollout, keeping the previous kernel installed and selectable in the bootloader, a reboot window, health checks and a rollback trigger

Executive Leadership Review

Candidates describe a final review by executive leadership to assess overall fit with the organization. Very little detail is reported about its format, so prepare for a conversation rather than a technical test: your essay stories, your open-source experience, your reasons for wanting a fully remote, written-first working style, and your practical questions about the contracting and travel arrangements candidates mention.

What to demonstrate

  • Overall fit with the organization, as the stage is described
  • Consistency between what you wrote earlier and what you say live
  • Your motivation and your account of working autonomously in a remote team

How to prepare

  • Re-read your written answers and make sure every claim can be expanded into a two-minute spoken story with a decision, a trade-off and a result
  • Prepare a concise answer on your open-source use and contributions, and one on how you work without daily oversight
  • Write down your questions about contract status, tax implications and in-person sprint travel so you can raise them calmly

Where candidates lose points

Submitting generic or typo-ridden written answers to the essay-style questions

Write each answer around one real project with a decision, a trade-off and an outcome. Candidates report that generic, machine-sounding answers and careless errors hurt, so keep your own voice and proofread after a break.

Racing through the Thomas GIA and losing points to simple mistakes

Practise each area (perceptual speed, number speed and accuracy, word meaning, spatial visualization) under a timer, and set a personal rule to keep accuracy high even if a few items go unanswered.

Reciting the Linux boot process as a list of names (BIOS, GRUB, kernel, systemd) with no mechanism

For each stage say what it loads and what it hands over: firmware initialises hardware and finds the bootloader, the bootloader loads kernel and initramfs, the kernel mounts the real root and starts PID 1, and systemd brings up targets. Be ready to name the commands and logs you would check at each stage.

Describing goroutines, decorators or Rust ownership with slogans instead of runtime behaviour

Prepare precise statements: goroutines are scheduled by the Go runtime onto a smaller set of OS threads; a decorator is a callable that replaces a function at definition time; Rust ownership gives each value one owner, with borrows checked at compile time. Add one short code example for each.

Answering the IoT monitoring agent or package metadata design like a generic web service

State the constraints first: memory and CPU budget and flaky connectivity for the agent; read-heavy mirrored metadata with stale-read tolerance for the package service. Then choose protocols, caching, batching and failure behaviour that follow from those constraints.

Questions & practice

What is the difference between a process and a thread in Linux, and how does the kernel schedule them?

Medium · Linux & Systems Engineering

Approach

  1. In Linux both are tasks (task_struct). fork() creates a child with its own address space using copy-on-write pages. A thread is created with clone() flags such as CLONE_VM, CLONE_FILES, CLONE_FS, CLONE_SIGHAND and CLONE_THREAD, so it shares memory, file descriptors and signal handlers with its group.
  2. Per thread: its own stack, registers, TID, signal mask and scheduling state. Threads in a group share one PID (the TGID), which is why ps shows one process and ps -L or /proc/<pid>/task shows its threads.
  3. The scheduler schedules each thread independently. The normal class was CFS (a red-black tree ordered by weighted vruntime, with nice values setting weights), replaced by EEVDF in kernel 6.6. Real-time classes SCHED_FIFO and SCHED_RR, and SCHED_DEADLINE, take priority over normal tasks.
  4. Trade-off: switching between threads of one process skips the address-space switch and is cheaper, and sharing memory avoids IPC. The cost is that shared state needs locking, and a fault such as SIGSEGV in one thread kills the whole process. Separate processes isolate crashes.
  5. Edge case to raise yourself: fork() in a multithreaded program copies only the calling thread, so locks held by other threads stay locked in the child. Call only async-signal-safe functions before exec.

Follow-up

  • What happens to a mutex held by another thread when a multithreaded process calls fork()?
  • How would you pin a latency-sensitive thread to a CPU, and what does it cost the rest of the system?
  • How do nice values change the CPU share two busy tasks get?

How does virtual memory management work in Linux, and what is the role of the Out-Of-Memory (OOM) killer?

Medium · Linux & Systems Engineering

Approach

  1. Each process gets its own virtual address space. Page tables map virtual pages (usually 4 KiB, or huge pages) to physical frames, the MMU translates and the TLB caches translations. Accessing an unmapped page triggers a page fault: minor if the page is already in memory, major if it needs disk I/O.
  2. Allocation is lazy: malloc and mmap reserve address space, and physical pages arrive on first touch. fork shares pages copy-on-write. That is why VSZ is far larger than RSS, and why the kernel can overcommit (vm.overcommit_memory: 0 heuristic, 1 always, 2 strict commit limit).
  3. Under pressure, kswapd and direct reclaim drop clean page cache, write back dirty pages and swap anonymous memory. When reclaim cannot free enough, the OOM killer picks the task with the highest oom_score (mostly its RSS, swap and page-table usage, shifted by oom_score_adj from -1000 to 1000) and sends SIGKILL.
  4. In containers a cgroup v2 memory.max limit triggers an OOM kill inside that cgroup only. Diagnose with dmesg or journalctl -k ('Out of memory: Killed process'), memory.events and /proc/<pid>/status. Userspace killers such as systemd-oomd act earlier using pressure-stall information (PSI).
  5. Trade-off to state: setting oom_score_adj to -1000 protects a critical process but can shift kills onto the services it depends on. Strict overcommit avoids surprise kills but makes allocations fail sooner.

Follow-up

  • What is the difference between VSZ and RSS, and which would you alert on?
  • A container is OOM-killed while the host has free memory. Why?
  • How would you protect a database from the OOM killer, and what is the risk?

What are Python decorators, how do they work under the hood, and when would you use them?

Medium · Programming & Language Semantics

Approach

  1. @dec above def f is shorthand for f = dec(f), run once at definition time. Functions are first-class objects, so a decorator is any callable that takes a function and returns a replacement, usually a closure wrapping the original.
  2. Write the wrapper as def wrapper(*args, **kwargs) so it passes everything through, and apply functools.wraps(f). That copies name, qualname, doc and module, updates dict and sets wrapped, so introspection, logging and tests still see the original.
  3. A decorator with arguments is a factory: @retry(times=3) calls retry(times=3) first, which returns the actual decorator, so there are three nested levels. When stacked, the decorator nearest def is applied first, and the outermost wrapper runs first at call time.
  4. Name real uses: functools.lru_cache and functools.cache, property, staticmethod and classmethod, plus retries, timing, authorisation checks and registering handlers in a dict. Classes can also be decorators by defining call, and class decorators receive a class.
  5. Edge cases: decorating an async def needs an async wrapper that awaits the call. State kept in the closure is shared across all calls and threads, so it needs a lock. lru_cache needs hashable arguments and holds references to them.

Follow-up

  • Write a retry decorator that takes the attempt count and exceptions to catch, with exponential backoff.
  • Why does lru_cache fail on a list argument, and what memory risk does it carry on a method?
  • How would you decorate a function so it works for both sync and async callers?

Explain the difference between goroutines and OS threads in Go. How does the Go runtime schedule them?

Medium · Programming & Language Semantics

Approach

  1. Goroutines are user-space tasks managed by the Go runtime. They start with a small stack (a few KiB) that grows by copying, while an OS thread gets a fixed stack reserved by the OS (often 8 MiB of virtual memory on Linux). Creating and switching goroutines costs far less, so many thousands can run.
  2. Scheduler model (G-M-P): G is a goroutine, M an OS thread, P a logical processor holding a local run queue. GOMAXPROCS sets the number of Ps, and so how many goroutines run Go code in parallel. There is also a global run queue, and idle Ps steal work from others.
  3. Blocking: in a blocking syscall, the M releases its P so another M can keep running that P's goroutines. Network I/O goes through the netpoller (epoll on Linux), so the goroutine parks and no thread is tied up. Channel and mutex waits park the goroutine, not the thread.
  4. Preemption: since Go 1.14 the runtime preempts long-running goroutines asynchronously using signals. Before that, a tight loop with no function calls could starve others.
  5. Trade-offs: goroutines make blocking-style code cheap, but they leak if a goroutine waits forever on a channel. Use context cancellation and watch runtime.NumGoroutine or pprof's goroutine profile. Use runtime.LockOSThread when a C library or thread-local OS state needs a fixed thread.

Follow-up

  • How would you find and fix a goroutine leak in a long-running service?
  • What should GOMAXPROCS be in a container with a CPU quota, and why does it matter?
  • Why can a cgo call or a blocking syscall increase the number of OS threads?

How does memory safety work in Rust without a garbage collector? Explain the concepts of ownership, borrowing,

Medium · Programming & Language Semantics

How does memory safety work in Rust without a garbage collector? Explain the concepts of ownership, borrowing, and lifetimes.

Approach

  1. Ownership: every value has exactly one owner, and the value is dropped (Drop runs, memory is freed) when the owner goes out of scope. Assigning or passing a non-Copy value moves it, so the old binding cannot be used, which rules out double frees.
  2. Borrowing: you may hold any number of shared references (&T) or exactly one mutable reference (&mut T), never both at once. The borrow checker enforces this at compile time, which prevents data races, iterator invalidation and mutating data someone else is reading.
  3. Lifetimes: the compiler proves no reference outlives the value it points to, which rules out dangling pointers and use-after-free. Annotations like fn longest<'a>(x: &'a str, y: &'a str) -> &'a str describe how input and output lifetimes relate; they do not extend how long anything lives. Elision rules cover common cases.
  4. Escape hatches and their cost: Box for heap ownership, Rc or Arc for shared ownership through reference counting (a cycle leaks unless one side uses Weak), RefCell for borrow checks moved to runtime (a violation panics), Mutex or RwLock across threads, and unsafe blocks where the programmer upholds the rules. Send and Sync mark which types may cross threads.
  5. Be precise about the guarantee: safe Rust prevents memory-unsafety and data races, not leaks (mem::forget and Rc cycles are safe) and not deadlocks or logic races.

Follow-up

  • Why won't the compiler let you push to a Vec while holding a reference to one of its elements?
  • When would you choose Rc<RefCell<T>> over restructuring ownership, and what does it cost?
  • What is the difference between Rc and Arc, and why is Rc not Send?

Write a function to validate an ISBN check digit and explain your approach to error handling and edge cases.

Medium · Programming & Language Semantics

Approach

  1. Normalise first: strip hyphens and spaces, then require length 10 or 13. Check characters explicitly against '0'-'9'. In Python, str.isdigit() also accepts characters like '²', which int() then rejects. Allow 'X' (value 10) only as the last character of an ISBN-10, and decide whether lowercase x counts.
  2. ISBN-10: weight the digits 10 down to 1 and accept when the sum mod 11 is 0. Example: 0-306-40615-2 gives 132 = 12 x 11. ISBN-13: weights alternate 1,3,1,3...; accept when the sum mod 10 is 0, equivalently check digit = (10 - sum of the first 12 weighted mod 10) mod 10. Example: 978-0-306-40615-7.
  3. Error handling: separate malformed input (wrong length, bad character, X in the wrong place, None or non-string) from a well-formed number with a wrong check digit. I would return a bool from is_valid and raise ValueError with a reason from a parse function, or return a result type, and say why.
  4. Complexity is O(n) time and O(1) extra space. Test with known-valid numbers, each single-digit change, swapped neighbours, empty string, all hyphens, an X in the middle, and 13-digit strings that pass the checksum but do not start with 978 or 979 if you enforce the prefix.
  5. Explain what each scheme catches: mod 11 with weights 10..1 detects every single-digit error and every adjacent transposition. The 1-3 weighting of ISBN-13 detects every single-digit error but misses adjacent transpositions of digits that differ by 5.

Follow-up

  • Extend the function to convert a valid ISBN-10 into its ISBN-13 form.
  • Why does ISBN-13 miss some adjacent transpositions that ISBN-10 catches?
  • How would you validate a large file of ISBNs and report which lines failed and why?

Explain the computational complexity and memory footprint of standard data structures like hash maps and slice

Medium · Programming & Language Semantics

Explain the computational complexity and memory footprint of standard data structures like hash maps and slices in your primary language.

Approach

  1. Pick your primary language and give average and worst case. Python dict: open addressing, average O(1) get, set and delete, O(n) worst case under heavy collisions, resize when about two-thirds full. Since 3.7 insertion order is guaranteed, thanks to the compact layout (an index array plus a dense entries array).
  2. Python list: a dynamic array of pointers. Index and append are O(1) (append amortised, through over-allocation), insert or pop at the front and the in operator are O(n). Memory: 8 bytes per slot on 64-bit plus each boxed object (a small int is 28 bytes), so array, bytes or NumPy are far denser for numbers.
  3. Go slice: a 24-byte header (pointer, len, cap) over a backing array. append is amortised O(1) and reallocates when cap is exceeded (doubling for small slices, a smaller factor for large ones). A subslice shares the backing array, so a small slice can keep a huge array alive, and appending through two slices can overwrite each other's data.
  4. Go map: a hash table with average O(1) operations and randomised iteration order. It is unsafe for concurrent writes (the runtime aborts with 'concurrent map writes'), so use a mutex or sync.Map. It does not shrink after deletes. Go 1.24 moved the implementation to Swiss tables.
  5. Show you can act on it: pre-size with make([]T, 0, n) or make(map[K]V, n), use a set for membership tests, use collections.deque for queue operations, and choose a sorted slice with binary search when memory matters more than O(1) lookup.

Follow-up

  • Why can a small subslice in Go keep hundreds of megabytes alive, and how do you fix it?
  • When would a sorted array with binary search beat a hash map in practice?
  • What makes a hash map degrade to O(n), and how do languages defend against hash-flooding inputs?

Find version gaps and relay lag with window functions

Medium · window functions · event tables · frame semantics

outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.

Approach

  1. Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
  2. Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
  3. Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
  4. Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.

Follow-up

  • Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
  • Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?

Denormalise tenant onto revisions and backfill it live

Hard · denormalisation · online migration · backfill

resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.

Approach

  1. Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
  2. Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
  3. Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
  4. Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.

Follow-up

  • The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
  • How do you verify the backfill actually finished, given rows are still being inserted while it runs?

Why are you interested in working at Canonical, and how do you align with our open-source mission?

Medium · Academic & Personal Background (The Writ

Approach

  1. Build the answer on concrete evidence. Name the Ubuntu releases, packaging formats (Debian packages, Snaps) or tools such as LXD, Kubernetes or cloud-init that you have actually run, and say what you did with them: a server you maintained, a package you built, a bug you filed.
  2. Prove open-source alignment with links rather than adjectives: merged patches, issues you reported with reproduction steps, a project you maintain. If you have none, describe the smallest upstream contribution you plan to make and the project it belongs to.
  3. Tie your own skills (Python, Go, Rust or C/C++, CI, packaging) to the kind of work candidates describe for this role: system packaging, cloud and container tooling, low-level debugging.
  4. Address remote, written-first work with one real example, such as a design doc or issue thread you wrote that settled a decision without a meeting.
  5. Avoid the mistake that sinks this answer: generic praise of open source, or echoing mission language back. Each paragraph should contain a fact only you could write.

Follow-up

  • Which open-source project would you contribute to first, and what would the first change be?
  • Tell me about a time an upstream maintainer rejected or reworked your change. What did you do?
  • What is one limitation you have hit in Ubuntu or its packaging tools, and how would you fix it?

Describe your academic performance in mathematics and physics during high school and university. Were you a to

Medium · Academic & Personal Background (The Writ

Describe your academic performance in mathematics and physics during high school and university. Were you a top performer?

Approach

  1. Answer with facts: the subjects, grades or marks, and class rank or percentile if you know it. If you do not know a rank, say so rather than estimating one.
  2. Be honest about weak results and give context in one sentence (illness, a change of school, working alongside study), then point to later quantitative evidence: a degree module, a competition, or work that needed probability, linear algebra or numerical methods.
  3. Connect the foundation to engineering with one specific example, such as using Big-O and amortised analysis to choose a data structure, or statistics to judge a performance regression against noise.
  4. Structure it as short paragraphs per stage (secondary school, university, since then) so a reader can scan it. Proofread every figure, because a reader can compare them against your CV.

Follow-up

  • Which quantitative course was hardest for you, and how did you handle it?
  • How have you kept your mathematical skills current since university?
  • Describe a time a mathematical argument changed an engineering decision you made.

What is your proudest technical achievement as a Software Engineer, and what specific challenges did you overc

Medium · Academic & Personal Background (The Writ

What is your proudest technical achievement as a Software Engineer, and what specific challenges did you overcome?

Approach

  1. Pick a project where you made the key technical decision, not one where you watched a team make it. Write in the first person: 'I profiled the importer and found...'.
  2. Open with the problem in measurable terms: the baseline (latency, failure rate, build time, memory) and the constraint that made it hard, such as a legacy API, no downtime, or tiny hardware.
  3. Spend most of the answer on the trade-off: the two or three alternatives you considered, why you rejected them, and what you gave up with the option you chose.
  4. Close with a measured result and how you verified it (a benchmark, a before/after metric, an incident count), then one thing you would do differently.
  5. Losing version: a list of technologies with no decision in it, or a team story where your own contribution cannot be separated out.

Follow-up

  • Which alternative did you reject, and under what conditions would it have been the better choice?
  • How did you confirm that your change caused the improvement and not something else?
  • What part of that system would fail first if load grew ten times?

Describe a leadership role you took on during your education or early career and the impact you made.

Medium · Academic & Personal Background (The Writ

Approach

  1. Choose a role where other people's results depended on you: teaching assistant, society or project lead, mentor, release coordinator, or owner of a migration that other teams had to adopt.
  2. Set the scope in one or two sentences: how many people, what goal, what authority you had and what you did not have.
  3. Centre the story on one hard moment, such as a missed deadline, a disagreement, or someone underperforming, and say exactly what you did and why.
  4. State the impact with evidence: a shipped result, retention, pass rates, time saved, then one lesson that changed how you lead now.
  5. Avoid listing titles without decisions, and avoid a story where the leadership was only organising meetings.

Follow-up

  • What would the team have done differently if you had not been there?
  • Tell me about someone who disagreed with your direction. How did it end?
  • How did you lead people you had no formal authority over?

Explain the step-by-step Linux boot process from the BIOS/UEFI to the user space.

Medium · Linux & Systems Engineering

Approach

  1. Firmware stage: BIOS runs POST and loads the first 512-byte sector (MBR) holding the boot loader's first stage. UEFI instead reads the GPT, mounts the FAT-formatted EFI System Partition and runs an EFI executable. With Secure Boot, a signed shim verifies and loads GRUB.
  2. Boot loader: GRUB reads grub.cfg, loads the compressed kernel (vmlinuz) and the initramfs into memory, and passes the kernel command line (root=, ro, quiet, plus any debugging flags).
  3. Kernel: it decompresses itself, sets up memory management, the scheduler and built-in drivers, unpacks the initramfs as a temporary root and runs its /init. That loads the modules needed to reach the real root (storage, LVM, LUKS unlock), mounts it, then calls switch_root and execs /sbin/init.
  4. User space: systemd runs as PID 1, builds the unit dependency graph and starts units in parallel until it reaches default.target (multi-user.target or graphical.target), which replaced SysV runlevels. getty or a display manager then offers login.
  5. Name the debugging tools at each stage: the GRUB menu and editing the command line, dmesg and journalctl -b for kernel and service logs, systemd-analyze blame or critical-chain for slow boots, and systemd.unit=rescue.target or init=/bin/bash for recovery.

Follow-up

  • The kernel panics with 'VFS: Unable to mount root fs'. What are the likely causes and how do you check them?
  • How does the chain of trust work under Secure Boot, and what happens with an unsigned kernel module?
  • How would you find out which unit is making boot slow?

How do you safely upgrade a Linux kernel on a production system, and what roll-back strategies would you emplo

Medium · Linux & Systems Engineering

How do you safely upgrade a Linux kernel on a production system, and what roll-back strategies would you employ?

Approach

  1. Before rollout: test the new kernel on staging hardware that matches production (same NICs, storage controllers, firmware). Check that out-of-tree and DKMS modules build for it, that /boot has space, and that the initramfs was generated.
  2. Roll out in waves: one canary host, then a small percentage, then the rest. Drain each host first (remove it from the load balancer, or cordon and drain it in Kubernetes) and set a health gate before each wave proceeds.
  3. Keep the previous kernel package and its GRUB entry installed. With GRUB_DEFAULT=saved, grub-reboot boots the new kernel once only, so a host that hangs returns to the known-good kernel on the next power cycle. Make it default with grub-set-default only after it passes checks.
  4. After reboot, verify uname -r, dmesg for driver errors, service health and a performance baseline. Keep out-of-band access (serial console, IPMI) and a hardware watchdog so a hung host can be recovered remotely.
  5. Mention the alternatives and their limits: kernel live patching applies security fixes without a reboot but does not replace a version upgrade, and A/B root partitions give an atomic rollback on appliances or IoT devices.

Follow-up

  • The new kernel boots but the storage driver fails on one hardware model. How do you contain it?
  • How would you automate this across a thousand hosts without losing capacity?
  • When would you choose live patching over a reboot, and what are its limits?

Explain how pipe syntax works under the hood in a Unix shell. How do processes communicate through pipes?

Medium · Linux & Systems Engineering

Approach

  1. For a | b the shell calls pipe(), which returns two file descriptors: a read end and a write end backed by a kernel buffer. It then forks two children.
  2. In the first child: dup2(write_end, STDOUT_FILENO), close both original ends, execve a. In the second: dup2(read_end, STDIN_FILENO), close both ends, execve b. The parent closes both ends and waitpids both. The programs never know they are in a pipeline.
  3. Kernel behaviour: on Linux the buffer is 64 KiB by default (adjustable with fcntl F_SETPIPE_SZ). write blocks when it is full and read blocks when it is empty, which gives natural backpressure. Writes of up to PIPE_BUF (4096 bytes on Linux) are atomic.
  4. EOF and errors: read returns 0 only after every write end is closed, so a stray open write end in the parent or reader makes the reader hang forever. Writing with no reader raises SIGPIPE (or EPIPE if ignored), which is how yes | head -1 terminates.
  5. Shell details: the pipeline's exit status is the last command's unless set -o pipefail is on. All stages run concurrently. For a|b|c the shell makes n-1 pipes. mkfifo creates a named pipe for unrelated processes.

Follow-up

  • Why does cat bigfile | head -1 finish quickly instead of reading the whole file?
  • What happens if the parent shell forgets to close the write end?
  • How would you implement a three-stage pipeline in C, and which descriptors must each child close?

How would you design a distributed, highly available package manager metadata service?

Medium · System Design & Architecture

Approach

  1. Clarify the workload: the read side is large numbers of clients fetching index files, mostly by polling for updates, while writes are rare publish events. The hard requirement is that a client never sees an index referencing files that are not yet available, and never gets tampered metadata.
  2. Publish path: upload package blobs to replicated object storage first, then write index files that are immutable and content-addressed (named by their hash, like apt's by-hash layout). Last, atomically replace one small signed top-level manifest listing the index hashes. Readers then always see a consistent snapshot.
  3. Read path: stateless frontends and a CDN or mirror network. The top-level manifest gets a short cache TTL, hash-named files can be cached indefinitely. Clients verify the manifest signature and every hash, so mirrors and caches do not need to be trusted.
  4. Availability: replicate storage across regions, run frontends in several regions behind health-checked DNS or anycast, and keep the publisher's metadata database (primary plus replicas) off the read path, so a database outage stops publishing but not installs.
  5. Name the security and bandwidth details: keep signing keys in an HSM or offline, put an expiry (Valid-Until) in the manifest to defeat replay or freeze attacks, and offer incremental diffs of large indexes so clients download less.

Follow-up

  • A mirror falls hours behind. What do its clients see, and how do you detect it?
  • How would you roll an update out to a percentage of clients first?
  • How do you rotate or revoke a compromised signing key without breaking existing installs?

Describe how you would build a lightweight monitoring agent that runs on thousands of IoT devices with limited

Medium · System Design & Architecture

Describe how you would build a lightweight monitoring agent that runs on thousands of IoT devices with limited resources.

Approach

  1. Pin the constraints first: RAM and CPU budget per device, storage type (flash wear), network (intermittent, metered, behind NAT), and how many devices report how often. Those numbers drive every later choice.
  2. Agent: a single static binary (Go, Rust or C) with a fixed memory ceiling. It reads metrics directly from /proc and /sys (stat, meminfo, loadavg, net/dev) instead of spawning processes, aggregates locally (counters, min/max, histograms) and sends compressed batches rather than one message per sample.
  3. Transport: the device pushes outbound because it sits behind NAT, over MQTT or HTTPS with mutual TLS and a per-device identity. Retry with exponential backoff plus jitter so a fleet does not reconnect all at once after a backend outage. Keep a bounded ring buffer in RAM, and spill to disk only with a size cap that drops low-priority data first.
  4. Self-protection: run under systemd with MemoryMax and CPUQuota and a watchdog, report the agent's own CPU and memory use, and ship config and agent updates signed, with an A/B or versioned rollback path.
  5. Backend: ingest partitioned by device ID into a time-series store with downsampling and retention tiers. Flag devices that go silent by comparing last-seen time against their expected interval, which is a different signal from a device reporting bad values.

Follow-up

  • The backend was down for an hour. What happens when thousands of devices reconnect, and how do you shape it?
  • How do you update the agent across the fleet without bricking devices?
  • How do you prove the agent's overhead stays within its budget?

How do you design an API gateway that handles rate limiting, authentication, and dynamic routing under high lo

Medium · System Design & Architecture

How do you design an API gateway that handles rate limiting, authentication, and dynamic routing under high load?

Approach

  1. Layout: a load balancer in front of stateless gateway instances. Each request passes through a fixed pipeline: TLS termination, authentication, coarse authorisation, rate limiting, route lookup, then proxying with timeouts, bounded retries and circuit breakers per upstream.
  2. Authentication without a network hop: verify JWT signature, exp, iss and aud locally against cached JWKS keys, refreshing them in the background. Opaque tokens need introspection, so cache the results briefly. Revoking a token before it expires needs short lifetimes or a small deny-list.
  3. Rate limiting: a token bucket (capacity sets the burst, refill rate sets the sustained limit) per API key or tenant. Across instances, keep the counters in Redis and update them atomically with a Lua script, or give each instance limit/N locally to skip the hop and accept approximation. Return 429 with Retry-After, and decide up front whether the limiter fails open or closed if Redis is down.
  4. Dynamic routing: a versioned route table (host, path, header to upstream cluster, with weights for canaries) in a config store such as etcd, pushed to gateways and swapped atomically in memory, so in-flight requests finish on the old table. Upstream endpoints come from service discovery and health checks.
  5. High load: an event-driven proxy with pooled keep-alive upstream connections, no synchronous remote calls on the hot path, and horizontal scaling. Measure per-route latency, 429 and 5xx rates, and propagate a request ID.

Follow-up

  • Redis latency spikes and the rate limiter slows every request. What do you change?
  • How do you revoke a user's access immediately when tokens are valid for an hour?
  • How do you push a routing change to every gateway without dropping in-flight requests?

Walk us through your approach to designing a system that automates the deployment of microservices across mult

Medium · System Design & Architecture

Walk us through your approach to designing a system that automates the deployment of microservices across multiple hybrid cloud environments.

Approach

  1. Clarify scope: how many services and teams, which environments (on-premises such as OpenStack plus AWS, GCP or Azure), whether every target runs Kubernetes, and the compliance limits on which data may live where.
  2. Build once, deploy many: CI builds an immutable image per commit, references it by digest, signs it and pushes it to a registry replicated into each environment, so on-premises sites do not pull across the internet at deploy time.
  3. Declarative desired state in Git (GitOps): a shared base manifest per service plus per-environment overlays (Helm values or Kustomize). An agent inside each cluster pulls and reconciles, so the central system needs no inbound credentials into on-premises networks, and drift is detected and corrected.
  4. Progressive delivery: promote through dev, staging, then production in waves by region or cloud. Use canary or blue-green with automated rollback on error-rate and latency thresholds. Change schemas in expand-then-contract steps so old and new versions run side by side.
  5. Failure handling: if the central controller or Git host is down, clusters keep running their last applied state. Secrets come from each environment's secret manager at runtime, never from the repo. Every deployment is auditable through the commit that caused it.

Follow-up

  • The rollout succeeds in one cloud and fails halfway in another. What state is the system in, and what do you do?
  • How do you coordinate a database migration with a service rollout across environments?
  • One environment runs plain VMs rather than Kubernetes. How does your design accommodate it?

Edge instances grow 400 MB per hour until the nightly restart

Hard · memory leak · heap analysis · metric cardinality

Edge API instances start at 700 MB resident and grow about 400 MB/hour; a nightly rolling restart has hidden it for weeks. Growth continues unchanged when request rate halves overnight, p99 degrades in the last hours before an instance is recycled, and heap used immediately after a forced full GC rises monotonically. The service holds no product state. Name the discriminating measurement that separates the plausible causes, give the most likely cause, and give the fix and how you would verify it.

Approach

  1. Separate resident memory from live heap first, because they fail differently. Resident size can grow from fragmentation, native buffers or thread stacks while the heap is flat; heap used after a full GC rising monotonically is the measurement that says objects are reachable and not being released. You already have it, so this is retention, not fragmentation, and that closes off half the candidate list.
  2. Use the rate's independence from traffic as the discriminator. Growth that continues at half the request rate rules out per-request objects that are merely slow to collect and points at a structure that grows with distinct values observed rather than with call volume. Write the candidates that have that property: a metrics registry keyed on a high-cardinality label, an unevicted cache, an interner, a per-key lock map.
  3. Take two heap snapshots an hour apart and diff by retained size, reading the dominator tree, not by allocation count or instance count. Expect one root holding a map with millions of entries, then follow the reference chain to the code that inserts and never removes. Allocation profilers point at churn, which is the wrong signal here.
  4. The candidate that fits this service is an observability label carrying an identifier, such as a request path recorded before templating so that /v1/resources/48213 becomes its own metric series. That grows with distinct ids seen, is independent of rate, and explains the late p99 degradation, since GC cost rises with the size of the live set.

Follow-up

  • Post-GC heap is now flat but resident size still creeps. What are you looking at, and does it matter?
  • How would you have detected this before an OOM, given the nightly restart masked the trend?

Suggested practice checklist

The plan follows the reported order: written assessment and cognitive speed first, then Linux internals, language semantics and system design, with a final day for full run-throughs and the leadership conversation.

Day 1 — Write the essay answers

  • Write full answers on why Canonical, your proudest technical achievement and a leadership role, each built on a specific project with a decision and a result
  • Write an honest answer on your mathematics and physics background and an outline of your open-source use
  • Proofread each answer aloud, then once more on paper

Deliverable: Four proofread written answers (motivation, proudest achievement, leadership role, academic background) and a one-page outline of your open-source use.

Day 2 — Cognitive speed and the open-source story

  • Run timed practice sets in number comparison, visual difference spotting, synonyms and antonyms, and mirrored shapes, recording accuracy per area
  • Write a three-minute spoken answer about your experience with Ubuntu and other open-source products, with one concrete episode
  • Record the answer and listen for name-dropping without detail

Deliverable: A scorecard of accuracy per GIA area and a recorded answer on open-source experience with one detailed episode.

Day 3 — Boot, processes and pipes

  • Explain the boot path from firmware to user space aloud, naming what each stage hands to the next
  • Explain processes versus threads, fork and exec, and how the kernel schedules runnable tasks
  • Explain how the shell creates a pipe, forks the commands and wires file descriptors, then demonstrate it with a short C or Python snippet using pipe, fork and dup2

Deliverable: A boot-sequence diagram, a process-versus-thread comparison, and a working pipe/fork/dup2 snippet.

Day 4 — Production Linux: upgrades, memory, troubleshooting

  • Write a kernel upgrade and rollback plan covering staging, keeping the previous kernel bootable, health checks and the rollback trigger
  • Explain virtual memory, overcommit, page cache and swap, and how the OOM killer picks a victim
  • Write a diagnosis sequence for high load average with low CPU utilization, naming the tool you would run at each step

Deliverable: A kernel upgrade runbook, a one-page memory and OOM explainer, a troubleshooting checklist, and a written answer to the memory-growth drill.

Day 5 — Language semantics and coding

  • In your primary language, write the ISBN check-digit validator with tests for hyphens, a trailing X, wrong length and non-digit input
  • Implement a concurrent LRU cache and explain your locking choice, then outline a Python log parser that outputs a JSON report
  • Write one-paragraph explanations of decorators, goroutines against OS threads, and Rust ownership, borrowing and lifetimes, with a code example for each
  • Tabulate hash-map and slice or list costs: lookup, append, resize and memory overhead

Deliverable: Tested ISBN validator and LRU cache code, three one-paragraph semantics explanations with examples, and a complexity table.

Day 6 — Infrastructure design

  • Design the package-manager metadata service: read and write paths, mirroring or caching, consistency and what happens when a region is down
  • Design the IoT monitoring agent: resource budget, collection interval, batching, offline buffering, authentication and updates
  • Design the API gateway (rate limiting, authentication, dynamic routing) and the hybrid-cloud microservice deployment system, each with a failure-mode list

Deliverable: Four design write-ups, each with requirements, components, trade-offs and a failure-mode list.

Day 7 — Full run-through and leadership conversation

  • Answer the proudest-achievement and leadership questions aloud from your written versions, then add what changed afterwards
  • Write your questions about contract status, tax treatment and in-person sprint travel, and re-read all your written answers for consistency

Deliverable: Two recorded spoken stories, two drill answers, and a written list of practical questions for the final conversation.

Essay and open-source stories

The reported behavioural material comes mostly from the written questionnaire: your motivation, your proudest technical achievement, a leadership role, your academic record and your experience with Ubuntu and other open-source products. The same stories are likely to resurface in live conversations, so keep them specific enough to survive follow-up questions and consistent between what you wrote and what you say.

  1. What is your proudest technical achievement as a Software Engineer, and what specific challenges did you overcome?
  2. Describe a leadership role you took on during your education or early career and the impact you made.
  3. Tell me about a time a maintainer or reviewer criticized your technical work. What did you change, and what did you leave as it was?
  4. Describe a project where you worked mostly in writing with people in other time zones. How did you keep decisions clear and traceable?

What is your experience with Ubuntu and other open-source products? Which ones do you use regularly?

Medium · Academic & Personal Background (The Writ

Approach

  1. Open with an honest inventory: name the Ubuntu releases or other open-source projects you really use, in what setting (laptop, servers, CI, containers), and for how long. Only name what you can discuss in depth, because every name invites a follow-up.
  2. Pick one episode that shows engagement beyond installing and using: a bug you reproduced and reported, a patch or documentation fix you sent, a package you built or maintained, or a time you read the source to explain behaviour. State the situation in two sentences, then spend the rest on what you did.
  3. If you have contributed, describe the mechanics: how you found the project's contribution process, what a minimal reproducer or test looked like, how review feedback changed your change, and what finally happened. If you have not, describe depth of use instead, such as debugging a failed service with the system journal, pinning or holding a package version, or working out why a containerized app behaved differently.
  4. Close with what the experience changed in how you work, such as writing clearer issue reports or reading upstream changelogs before upgrading. Avoid the common failure of listing tools without a story or overstating a one-line fix as a major contribution.

Follow-up

  • Walk me through a bug report or patch you sent: what did you include, and what did the maintainers ask you to change?
  • What is one thing about Ubuntu or a tool you use that you would change, and how would you go about proposing it upstream?
  • How did you handle review feedback you disagreed with?

Tell callers you do not own that their integration breaks

Medium · deprecation · compatibility · stakeholders

A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.

Approach

  1. Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
  2. Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
  3. Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
  4. Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.

Follow-up

  • How would you detect a consumer that reads the field only during a monthly export?
  • One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?

Ship under a deadline and bound the debt you chose

Medium · pagination · technical debt · tradeoffs

You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.

Approach

  1. Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
  2. Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
  3. Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
  4. Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.

Follow-up

  • At what page depth does the offset version breach your latency budget, given your page size and row counts?
  • What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?