cswork / libraryOpen practice workspace

Interview guides · Adagio Medical

Adagio Medical Software Engineer Interview Questions & Guide 2026

Software engineer interview prep for Adagio Medical: original coding, system design and debugging practice, each with a worked solution.

The 60-second brief

The Software Engineer role at Adagio Medical is a critical function within a team dedicated to advancing medical technology. As part of a company focused on innovative treatments, your work directly influences the reliability, safety, and performance of products designed to improve patient outcomes. You will be expected to bridge the gap between complex engineering requirements and functional, high-quality software solutions.

The shape of the workload matters more for your prep than the industry label does. Read-heavy serving, write-heavy ingestion and scheduled batch processing have different binding constraints and fail in different places, so find out which one the team lives in before picking design topics.

Focus areas

  • Make HL7 and X12 ingestion idempotent under replay
  • Hold accumulator limits under concurrent claim adjudication
  • Model bitemporal coverage and retroactive eligibility changes

Overview

This position demands a high level of technical rigor and the ability to operate in an environment where precision is paramount. Because the work often supports specialized medical hardware, you will collaborate closely with cross-functional teams, including clinical and manufacturing engineers, to ensure that software systems integrate seamlessly with the broader technology stack. Success in this role requires not only technical proficiency but also a dedication to the mission of the organization.

Adagio Medical operates in a highly regulated industry. Expect your technical work to be viewed through the lens of safety, compliance, and clinical reliability.

The Adagio Medical interview process

Adagio Medical candidates report 2 rounds · ≈ 2-4 weeks. The stages below are what candidates describe, not a published process.

Initial Outreach

An unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.

What to demonstrate

  • Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
  • Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
  • Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating

How to prepare

  • Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
  • Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
  • Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes

In-Person Panel Interview

Nobody in the room with you decides this. Interviewers typically write their rounds up separately, often before seeing anyone else's, and the outcome is settled later from those write-ups. A split panel gets resolved by whichever note carries specific evidence, so what you want out of each room is one concrete thing that person could write down: a bug you caught yourself, a trade-off you named, a decision you owned. The rest is arithmetic. The project you describe in a behavioural conversation is often the same system you sketched an hour earlier, and the two accounts have to agree.

What to demonstrate

  • Whether the scale, team size and timeline you attach to a project hold steady when that project resurfaces in a different round
  • Whether each interviewer leaves with a specific thing to cite rather than a general impression of competence
  • Whether a trade-off you defended in one round survives a challenge in another, instead of being quietly swapped for the answer the new interviewer seemed to want
  • Whether a question you have already answered earlier in the day gets the same answer at the same depth, without visible impatience

How to prepare

  • Write a one-page sheet per project fixing the figures you will quote — request volume, data size, team size, elapsed time, what broke — and say them aloud from the sheet until they come out identical every time
  • For each round on the schedule, decide in advance the one sentence you want in that person's notes, then check in a mock that you said it outright instead of leaving it to be inferred
  • Have someone ask you the same project question twice, an hour apart, and diff the two answers for numbers that moved or a trade-off that reversed

Where candidates lose points

Writing the access audit record inside the read transaction, or firing it off after the response with no durability.

Inside the transaction, an audit-store outage blocks clinical reads and turns a logging dependency into a care outage. Fire-and-forget afterwards means the audit trail is incomplete during precisely the incidents it exists to reconstruct, and the gaps are invisible until someone asks for the log. The usual resolution is committing the access decision and its audit row together to a local outbox and shipping asynchronously, which keeps the read path available while preserving durability.

Caching an eligibility answer with a long time-to-live and without an as-of date.

Coverage terminates retroactively as a matter of routine: an enrolment file received on the fifth of the month can terminate coverage effective the first. A day-long cache means services are delivered against a 'covered' answer that was already false when it was served, and the denial arrives weeks later. The answer needs to be keyed on person, plan and service date, carry the date it was computed as of, and expire fast enough that the exposure window is a decision rather than an accident.

Answering a debugging question with a guess instead of a bisection

Give a procedure that halves the search space at each step: confirm the symptom reproduces, establish the last known-good version, input or timestamp, then bisect over commits, over the data, or over the layers of the request path. A plausible cause with no way to confirm it is the same move whether it happens to be right or wrong, which is why it scores nothing.

Treating a network call as though it were a local function call

A remote call can be slow, fail, or return after you stopped waiting, so name the timeout, the retry policy, and what the caller sees while the dependency is down. A call with no timeout turns one slow dependency into an exhausted thread or connection pool in every service upstream of it.

Questions & practice

Merge twelve resource streams into one patient summary page

Medium · k-way merge · heap · pagination

A patient summary fans out to between 8 and 12 resource types. Each returns a network-backed, paged iterator of resource versions sorted by issued_ts descending, up to 200,000 versions per type for one person. Return the 50 most recent current versions across all types, where current means no later version supersedes it within the same logical resource, and a logical resource whose current version is entered_in_error is omitted entirely. You may not materialise the iterators. Give time and space in terms of k types, the result size and the page size.

Approach

  1. k-way merge with a max-heap holding one head per iterator, keyed on issued_ts. Seeding is O(k), each pop is O(log k), so reaching R emitted rows costs O(k + P log k) for P pops, with O(k) heap space plus one page buffered per iterator. Fetching everything and sorting is O(V log V) over V up to 2.4 million versions and drags every page across the network to produce 50 rows.
  2. Suppress with a hash set of logical resource ids already seen, recorded on first sight whether or not that version is emitted. A logical resource has exactly one resource type, so all of its versions arrive on one iterator, and that iterator is descending in issued_ts: the first version you see for a logical resource is its newest. Deciding on first sight and suppressing every later pop for that id is therefore correct in one pass with no lookahead.
  3. Count emits, not pops. A correction-heavy chart can burn many pops per emitted row, so a loop that stops at 50 pops returns a short page. Put a bound on total pops as well, and when it trips, return what you have with a continuation token rather than spinning.
  4. Break issued_ts ties deterministically on (resource type, resource id). Without it two identical requests return two different orderings and the next page silently skips or repeats rows.
  5. Handle entered_in_error at first sight: the erroneous version still supersedes its predecessor, so record the id in the seen set and emit nothing, dropping the whole logical resource instead of falling back to the value it replaced. Recording it is the load-bearing half. Skip it and the next pop re-displays the value a clinician already retracted.
  6. Summarise the budget honestly: the composite p99 is what the user feels, and it is bounded below by the slowest of the k iterators, so the merge fixes the ordering cost but not the fan-out tail.

Follow-up

  • One of the twelve iterators has a p99 of 400ms while the rest return in 20ms. What is your composite p99 and what would you change first?
  • The user pages to rows 51 through 100. How do you resume without re-reading from the top, and what breaks if issued_ts is not unique?
  • One resource type is accidentally returning ascending order. How would your code detect that rather than quietly emitting the oldest rows?

Resolve enterprise identity from a link log that supports unmerge

Hard · union-find · rollback · identity resolution

person_identity_link holds link_id, enterprise_person_id, assigning_authority, source_person_id, match_score, link_status (auto_linked, manual_linked, potential_duplicate, unlinked, rejected), version, superseded_by_link_id, decided_by, decided_at. You have up to 40 million source identities and 60 million decisions applied in decided_at order. Build a structure answering which enterprise identity a given (assigning_authority, source_person_id) resolves to after any prefix of the log, and supporting a revert of the k most recent merges at O(1) each. State the query complexity, and say why path compression is unavailable to you.

Approach

  1. Intern the node key as the pair (assigning_authority, source_person_id) into a dense integer index. Never key on source_person_id alone: two facilities in one network routinely issue the same medical record number to different people, so a bare-value key merges two patients before any matching logic has run, and a single-source test fixture will never show it.
  2. Classify the log rows before touching the structure. auto_linked and manual_linked are unions; potential_duplicate and rejected are recorded decisions that must not union anything; unlinked is a revert of the link it supersedes, not a new edge.
  3. Use union by size with an explicit undo stack, pushing (attached_root, its previous parent, the previous size of the absorbing root) on every union. Find walks parent pointers to the root: union by size bounds tree height at log2(n), so a query is O(log n) worst case, a union is O(log n), and a revert pops two words and is O(1).
  4. Path compression is the thing you have to give up, and the reason is specific: it rewrites parent pointers of nodes that were never named in the union being recorded, so the undo entry no longer describes the mutation that happened and a revert restores a forest that is quietly wrong. Near-constant find is only available to an append-only structure.
  5. Hold enterprise_person_id as a property of the root slot rather than a value copied onto every member. A merge then relabels one slot instead of n rows, and an unmerge restores two labels instead of reconstructing n.
  6. State the price plainly: every read path gains a resolution hop and no downstream table can carry a plain patient_id column. That cost is what buys reversibility, and it belongs in the design discussion rather than being discovered later by whoever writes the first join.

Follow-up

  • A merge is found wrong three weeks and 200,000 unions later, and your stack only reverts a suffix. What do you do instead, and what does that cost?
  • A claim was posted under the losing identity before the merge. After the unmerge, which identity owns it, and what in your design let you answer that?
  • Two matcher instances submit unions concurrently. What is the smallest change that keeps both the structure and the undo stack consistent?

Flatten overlapping coverage spans into a primary-payer timeline

Medium · sweep line · intervals · bitemporal

For one enterprise_person_id you hold up to 10,000 coverage_span rows: coverage_id, payer_id, plan_id, effective_date, termination_date (null means open-ended), coverage_order (1 is primary), status, valid_from and valid_to (system time). Given a system instant T and a date range D0 to D1, return disjoint business-date intervals covering that range, each labelled with the active coverage_id of lowest coverage_order, and with uncovered stretches returned explicitly. termination_date is the last covered day. Only status active counts. Target O(N log N) time, O(N) space.

Approach

  1. Apply the system-time slice before any interval logic: keep rows where valid_from is at or before T and valid_to is after T. That single predicate is what makes the answer 'what we believed at T' rather than 'what we believe now', and a retroactive termination loaded after T leaks straight into the output if you skip it.
  2. Convert each surviving row to a half-open day interval from effective_date up to termination_date plus one day, using positive infinity for a null termination. Half-open removes every off-by-one at the join between a termination and the next plan's effective date, which is where same-day switches get turned into false gaps.
  3. Sweep: emit 2N boundary events, sort by date in O(N log N), and hold the active set in a min-heap keyed by (coverage_order, coverage_id) with lazy deletion, popping the top while it has already expired at the current boundary. Each coverage is pushed once and popped once, so the sweep is O(N log N) total and O(N) space.
  4. Emit an output interval whenever the heap top changes between consecutive boundaries, and emit an explicit uncovered interval when the heap empties. A gap reported as a gap is a different answer from a gap omitted, and downstream the difference is a denial versus a silent assumption of coverage.
  5. Fix the tie-break and state it: lowest coverage_order, then lowest coverage_id. Two rows at order 1 is a data error, and without a deterministic rule the coordination-of-benefits answer changes between two runs over identical data.
  6. Clip to D0 and D1 last, so a coverage that starts before D0 still contributes its correct order inside the window.

Worked solution (30 min)

  1. Filter to rows live at T, then map each to a half-open interval and a (coverage_order, coverage_id) priority.
  2. Build the boundary list, sort it, and sweep with a lazily-deleted min-heap.
  3. At each boundary, drop expired heap tops and compare the new top to the previous one, emitting an interval on change.
  4. Emit an uncovered interval whenever the heap is empty between two boundaries.
  5. Clip the emitted list to D0 through D1 and assert the intervals are disjoint and contiguous.

Expected result: Window 2026-01-01 to 2026-12-31 with three live rows: coverage 1 at order 1 effective 01-01 terminating 06-30, coverage 2 at order 2 effective 03-01 open-ended, coverage 3 at order 1 effective 09-01 open-ended. Output is three intervals: 01-01 to 06-30 labelled coverage 1, 07-01 to 08-31 labelled coverage 2, 09-01 to 12-31 labelled coverage 3, with no gaps. Delete coverage 2 and the middle interval becomes an explicit uncovered stretch of 07-01 to 08-31.

Verify it yourself

  • The emitted intervals are sorted, pairwise disjoint, and their union is exactly D0 through D1 once uncovered stretches are included.
  • Every boundary date in the output is D0, the day after D1, some effective_date, or some termination_date plus one day. Any other date means a boundary was synthesised.
  • Removing the lowest-order coverage promotes exactly the next order and leaves every boundary it did not touch unchanged.
  • Choosing T before the load time of a retroactive termination returns the pre-correction timeline, and T after it returns the corrected one, from the same table.

Follow-up

  • Run the same query at two system instants and diff the timelines. What did the correction change, and what does each extra instant cost you?
  • A termination arrives with an effective date earlier than the service date of a claim that already adjudicated and paid. What does the timeline now say, and what has to happen to that claim?
  • Two enrolment files disagree on coverage_order for a dependent. What does your tie-break do, and what should the system do instead of tie-breaking?

Add a backfilled NOT NULL column to a live encounter table

Hard · online migration · ddl locks · backfill

encounter holds 900 million rows and is written continuously by the admit/discharge/transfer consumer. You must add admission_source_code text, backfill it per row from a lookup on the source message archive, constrain it to a fixed code set, make it NOT NULL, and add an index on (facility_id, admit_ts) — with no write outage and no statement blocked longer than one second. Give the ordered steps, the lock level each takes, why the obvious single ALTER is unavailable here, and the session setting that stops a DDL statement from stalling everything behind it.

Approach

  1. Say why the shortcut does not apply. Since PostgreSQL 11, ADD COLUMN with a constant default is metadata-only and cheap, but this value is derived per row from an archive lookup, so there is no constant to store and the table must be written row by row regardless.
  2. Add the column nullable first: catalog-only, ACCESS EXCLUSIVE for microseconds. The danger is not the statement's duration but the lock queue — a pending ACCESS EXCLUSIVE blocks every reader that arrives behind it, so a long-running report turns a microsecond DDL into a multi-minute outage. SET lock_timeout = '1s' before the ALTER and retry on failure; that converts a potential outage into a retried statement.
  3. Deploy dual-write before backfilling. The consumer must populate the column on every insert and transfer from that moment, or the backfill chases a moving tail forever.
  4. Backfill in bounded batches over primary-key ranges, a few thousand rows per transaction with a pause between, restricted to WHERE admission_source_code IS NULL so it is resumable and idempotent. One giant UPDATE holds a transaction open for hours, pins the xmin horizon so autovacuum cannot clean anything, and bloats the table by a full row version per updated row.
  5. Add the value constraint as CHECK (...) NOT VALID first — ACCESS EXCLUSIVE, no scan — then ALTER TABLE ... VALIDATE CONSTRAINT, which takes only SHARE UPDATE EXCLUSIVE and scans without blocking reads or writes.
  6. Reach NOT NULL without a blocking scan: add CHECK (admission_source_code IS NOT NULL) NOT VALID, VALIDATE it, then SET NOT NULL, which on PostgreSQL 12 and later uses the validated constraint as proof and skips the full scan; drop the now-redundant CHECK afterwards. Build the index with CREATE INDEX CONCURRENTLY, which takes SHARE UPDATE EXCLUSIVE, makes two passes plus a wait for concurrent transactions, cannot run inside a transaction block, and leaves an INVALID index behind on failure that must be dropped and rebuilt.

Follow-up

  • CREATE INDEX CONCURRENTLY has been running for four hours and you need to cancel it. What state is the index left in, how do you detect it, and what do you run next?
  • Your backfill is at 40% and the replica is 90 seconds behind. What do you change, and what do you measure to decide the new batch size?
  • The code set gains a value two weeks later. Does your CHECK constraint or a lookup table with a foreign key make that change cheaper, and what does each cost on the write path?

Reproduce and fix a lost update on a deductible accumulator

Medium · isolation levels · lost update · lock contention

An accumulator row holds deductible_applied_cents bigint and plan_deductible_cents bigint, keyed by (enterprise_person_id, plan_id, benefit_year). The adjudicator opens a transaction, SELECTs the row, computes the member's share in application code, then UPDATEs the row to the absolute new total it computed, all under PostgreSQL's default READ COMMITTED. Two claim lines for one member adjudicate concurrently: both charge the member deductible, but the accumulator advances by only one of the two amounts, so the member is billed deductible again on a later line after the plan deductible has already been met. Write the exact two-session interleaving that produces it. Then give three fixes, each naming the lock or isolation level, the SQLSTATE you must handle, and the throughput cost.

Approach

  1. State the anomaly precisely. READ COMMITTED takes a fresh snapshot per statement, so it prevents dirty reads but permits a lost update when a transaction reads a value, computes outside the database, and writes back an absolute result. The vulnerable window is the round trip through application code between two statements, not the transaction boundary — the same logic expressed as one relative UPDATE is safe at this very isolation level.
  2. Write the interleaving as an ordered script both sessions can be replayed from, with the commit points marked, and make both writes absolute (SET deductible_applied_cents = :computed_total). The point is not that two writes happen — it is that the second write stores a total computed from a value that had already been superseded by the time it landed.
  3. Fix one, SELECT ... FOR UPDATE on the read: the second session blocks on the row lock and, under READ COMMITTED, re-reads the newest committed version when it unblocks, so its computation starts from the winner's total. No serialization error to handle. Cost is serialised throughput per member and a lock held for the transaction's whole duration, so nothing inside may call an external payer.
  4. Fix two, REPEATABLE READ or SERIALIZABLE with a retry loop: in PostgreSQL, REPEATABLE READ aborts the second writer of a row with SQLSTATE 40001, 'could not serialize access due to concurrent update'; SERIALIZABLE additionally aborts on read/write dependencies detected by SSI, also under 40001. The retry re-runs the whole transaction, so the claim application must be idempotent or keyed by claim_line_id, or a retry double-applies.
  5. Fix three, single-writer partitioning: route by hash(enterprise_person_id) to one consumer per partition, so no two transactions ever touch one accumulator. No locks, no retries; the cost is head-of-line blocking behind a slow claim, rebalancing during deploys, and losing the ability to scale a hot member.
  6. Name the cheapest option and its real catch. A single UPDATE ... SET deductible_applied_cents = LEAST(plan_deductible_cents, deductible_applied_cents + :amt) ... RETURNING deductible_applied_cents does not lose the update: under READ COMMITTED an UPDATE that hits a concurrently updated row blocks, then re-evaluates its expressions against the newly committed version, so both increments land. The catch is that the member's share is the delta this statement actually applied, which is the returned value minus the row's pre-image — and RETURNING does not expose the pre-image before PostgreSQL 18's OLD/NEW aliases. On earlier versions you must read that pre-image under the same row lock, which puts you back in fix one with a shorter critical section. The one-statement form is a clean fix only when the caller does not need the delta.

Worked solution (35 min)

  1. Open two psql sessions against a scratch database and seed one accumulator at deductible_applied_cents = 40000, plan_deductible_cents = 100000.
  2. Run the interleaving by hand with both writes absolute, and confirm the row ends at 70000 while the two lines between them charged the member 55000 of deductible — the serial answer is 95000, so 25000 of applied deductible has been lost.
  3. Run the control: repeat the identical interleaving with both UPDATEs written as SET deductible_applied_cents = deductible_applied_cents + :amt, and confirm the row ends at 95000. That form does not lose the update at READ COMMITTED, which is how you know the defect is the read-modify-write in application code rather than the isolation level by itself.
  4. Re-run the absolute version with FOR UPDATE on both SELECTs and confirm session two blocks, re-reads 65000, and writes 95000.
  5. Re-run both sessions at REPEATABLE READ and capture the exact error text and SQLSTATE from the losing session.
  6. Write the retry wrapper and show what makes the retry safe: a uniqueness key on (accumulator_key, claim_line_id) in an application ledger, so a re-run of an already-applied line is a no-op rather than a second application.
  7. Write the one-statement LEAST form with RETURNING and say where the member's share now comes from, given RETURNING carries no pre-image before PostgreSQL 18.

Expected result: Seed: deductible_applied_cents = 40000, plan_deductible_cents = 100000. Interleaving: T1 BEGIN; SELECT deductible_applied_cents -> 40000; application computes line A's deductible share as 25000 and the new total as 65000. T2 BEGIN; SELECT -> 40000, because T1 has not committed and READ COMMITTED shows only committed rows; application computes line B's share as 30000 and the new total as 70000. T1 UPDATE accumulator SET deductible_applied_cents = 65000 WHERE key...; COMMIT. T2 UPDATE accumulator SET deductible_applied_cents = 70000 WHERE key...; it blocks on T1's row lock, and on unblocking writes its absolute 70000 over the committed 65000; COMMIT. The row reads 70000 while the two lines charged the member 55000, so the serial answer is 95000 and T1's 25000 is lost; the accumulator understates by 25000 and the member is billed deductible again after the 100000 is already met. The write must be absolute for this to happen — SET deductible_applied_cents = deductible_applied_cents + :amt blocks, re-reads the committed row and applies both increments. Fixes: (1) SELECT ... FOR UPDATE on the read, no SQLSTATE to handle, serialises per member for the transaction's whole duration and therefore forbids external calls inside it; (2) REPEATABLE READ or SERIALIZABLE plus retry on SQLSTATE 40001 ('could not serialize access due to concurrent update' under REPEATABLE READ; SERIALIZABLE raises the same SQLSTATE for read/write dependencies), which needs an idempotency key because the whole transaction re-runs; (3) hash-partition claims by enterprise_person_id to one writer per partition, zero contention at the cost of head-of-line blocking and rebalancing.

Verify it yourself

  • The unfixed script reproduces 70000 on a real database rather than on paper, and replaying the same two lines serially reproduces 95000.
  • The control run with in-SQL relative increments, under the identical interleaving, ends at 95000 — that isolates the defect to the application-side read-modify-write and not to READ COMMITTED itself.
  • With FOR UPDATE, session two's SELECT visibly blocks until session one commits, and the final value is exactly 95000.
  • At REPEATABLE READ the losing session reports SQLSTATE 40001 with 'could not serialize access due to concurrent update', and its transaction is left in an aborted state requiring ROLLBACK.
  • Applying the same claim_line_id twice through the retry path leaves the accumulator unchanged on the second attempt.

Follow-up

  • A claim is reversed six months later under a plan design that has since changed. What amount does the reversal subtract, and where is that number stored?
  • Your retry loop hits 40001 repeatedly for one member during a batch window. What is the backoff, and at what point do you stop retrying and pend the claim?
  • Which of the three fixes survives a retroactive eligibility change that invalidates everything applied in the last month, and what does the rebuild look like?

Can you describe a complex technical challenge you solved in a previous role?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  2. State your assumptions explicitly before working the problem.
  3. Work from the requirement backwards to the design.

Follow-up

  • What assumption would you test first?
  • How would you know your answer was wrong?

Model an order placement so a lost response stays recoverable

Hard · idempotency · partial failure · reconciliation

An ordering system POSTs a medication or lab order to /orders. It assigns a placer identifier; the performing system later assigns a filler identifier, and neither side controls both namespaces. The caller times out after 3s and cannot distinguish an order that never arrived, one that was accepted with the acknowledgement lost, and one still in flight. Duplicating an order is a patient-safety event and silently abandoning one is worse. Design the contract so the caller can always determine the true outcome without guessing, and specify its behaviour at the timeout, on retry, and after repeated failure.

Approach

  1. Remove the ambiguity at its source by letting the caller name the resource before it exists, so the identity of the attempt never depends on our response arriving. The placer identifier is already caller-assigned and unique within the caller's namespace, so the server key is (placer_namespace, placer_order_id) under a unique constraint. A timeout stops being an unknown and becomes a question with a stable key.
  2. Prefer the shape that makes the retry trivially safe. PUT /orders/{placer_namespace}/{placer_order_id} is idempotent by HTTP semantics and needs no key header; POST /orders needs an Idempotency-Key to reach the same place. Under either, the write is one INSERT ... ON CONFLICT DO NOTHING, because a check-then-insert lets two retries through under READ COMMITTED. A repeat with an identical canonical body returns the existing resource; a repeat with a different body is 409 rather than a silent replace, since an order is not a document to overwrite.
  3. Give the caller a read that resolves a timeout without writing: GET on the same key returns accepted, routed, filled with its filler_order_id, or rejected with a reason, and 404 means we genuinely never saw it. Without that read, the caller's only instrument is another write, which is precisely the behaviour being designed out.
  4. Be honest about what acceptance means. Accept is durably persisted and queued for the performing system, not performed, so return 202 with the order resource and a status the caller polls or subscribes to. Returning 201 for an order that is not yet routed makes the caller believe a stronger fact than is true, and the filler identifier arrives later on the performing system's own timeline.
  5. Write the caller's behaviour explicitly, because the contract is only half the design: at timeout, GET the key; on 404, retry the write with the same key; on 5xx, exponential backoff with full jitter up to a bounded attempt count; after five failures, stop and raise a human-visible alert carrying the placer identifier. A queued-for-human state is a better outcome than either a duplicate order or a silent drop, and this domain cannot absorb the drop.

Worked solution (40 min)

  1. Draw the three timelines a 3s timeout can hide - request lost, work done and response lost, work still in flight - and mark what the caller can distinguish in each, with and without a caller-assigned key.
  2. Write the resource path, the unique constraint, and the single atomic statement that performs the write.
  3. Write the state machine the GET exposes, state what 404 means, and name the one state that must never be inferred from a timeout.
  4. Write the caller's pseudocode for timeout, retry, backoff and give-up, with the attempt count and the alert payload.
  5. Run two concurrent retries of the same placer identifier against a real database and confirm one order exists and both callers observe the same state.

Expected result: PUT /orders/{placer_namespace}/{placer_order_id} under UNIQUE (placer_namespace, placer_order_id), written by one INSERT ... ON CONFLICT DO NOTHING; a repeat with the same canonical body returns the existing resource, a repeat with a different body returns 409. GET on the same key returns {status in accepted | routed | filled | rejected, filler_order_id when known, reason when rejected}, with 404 meaning never seen. Acceptance is 202 - durably queued, not performed. Caller: on timeout GET the key; on 404 retry the same key; on 5xx back off with full jitter for five attempts, then stop and alert with the placer identifier rather than resubmitting.

Verify it yourself

  • No outcome requires the caller to guess; every timeout resolves into a read.
  • Two concurrent retries of one placer identifier leave exactly one order.
  • A 202 is never returned for an order that is not durably persisted.
  • The give-up path terminates in a human-visible state rather than a dropped order.

Follow-up

  • The performing system returns a filler identifier for an order we have no record of. What do you do with it?
  • The caller reissues an old placer identifier for a genuinely different order after a counter rollover. What breaks, and how would you detect it?
  • Where does the record of the failed attempts live, and what must it carry for an incident review?

Duplicate results appear only in the hour after interface restarts

Hard · race condition · idempotency · read committed

About one in 40,000 ingested results produces a duplicate observation_result row. It happens only in the hour after an interface engine restarts and never reproduces in tests. The consumer checks a ledger table for the idempotency key, then writes the observation, then inserts the ledger row in a second transaction. The ledger has a unique index on the key, and the losing insert logs duplicate, ignoring. Adding a debug log between the check and the write made duplicates more frequent. Diagnose and fix.

Approach

  1. Run the query that splits the hypotheses before anything else: for one duplicate pair, compare their idempotency keys. Identical keys mean the key is right and mutual exclusion is broken. Different keys mean the key itself is wrong — a sender reusing or rotating control identifiers — which is a different bug with a different fix. Most time lost on this class of problem is lost by skipping this step.
  2. Measure the gap between the pair. created_at deltas in the tens of milliseconds, attributed to two different worker identifiers, mean two workers processed the same replayed message concurrently. Deltas of minutes or hours mean the ledger row was never durably written — rolled back, or written in a transaction that later aborted — and a later replay legitimately re-processed the message.
  3. Read the transaction boundaries rather than the isolation level. The check, the clinical write and the ledger insert span two transactions, so the window between the check and the ledger insert is unprotected at every isolation level, READ COMMITTED and SERIALIZABLE alike. The unique index protected the ledger; it never protected the observation, which had already committed by the time the conflict was detected.
  4. Explain why instrumentation moved it. The debug log widened the window between the check and the ledger insert, so more concurrent pairs landed inside it. Deleting the log narrows the window and hides the defect without fixing it — the same reason a single-threaded test suite is green, and the reason the incident correlates with restarts, when the broker redelivers a window of messages at once.
  5. Fix by turning check-then-act into one atomic conditional write: in a single transaction, INSERT INTO ingest_ledger (idem_key, ...) VALUES (...) ON CONFLICT (idem_key) DO NOTHING RETURNING idem_key, and if no row comes back, do nothing further. The observation insert belongs in that same transaction. Under READ COMMITTED the second inserter blocks on the unique index until the first commits and then sees the conflict, so the index — not the isolation level — supplies the mutual exclusion.
  6. Prove it with a deliberate concurrency test: two sessions released by a barrier on the same key, several thousand rounds, asserting exactly one observation row each time, then replay a captured restart window and diff row counts against a clean run.

Follow-up

  • The clinical write must go to a different store than the ledger. How do you keep this atomic without a distributed transaction, and what does the outbox look like?
  • The sender's control ID counter rolls over and begins reusing values. What breaks in your key, and what production signal would tell you it happened rather than a customer telling you?

Suggested practice checklist

For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.

Day 1 — Numbers before diagrams

  • Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
  • Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
  • Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.

Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.

Day 2 — One system, from requirements to schema

  • Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
  • Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
  • Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.

Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.

Day 3 — The consistency you are actually buying

  • Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
  • Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
  • Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.

Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.

Day 4 — Failure is the design

  • For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
  • Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
  • Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
  • Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.

Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.

Day 5 — Scaling the hot path

  • Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
  • Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
  • Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
  • Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.

Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.

Day 6 — Keep the coding hand in, at the bar that applies to you

  • Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
  • Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
  • Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.

Deliverable: One problem hardened to review standard, with its test list and one written review comment.

Day 7 — Defend it while being interrupted

  • Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
  • Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
  • Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.

Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.

Outage answers are graded on instruments

Nobody is scoring your stamina at three in the morning. What carries weight is which signal told you something was wrong, what you measured before touching anything, what you rolled back versus what you fixed forward, and why you picked one. 'We restarted it and it went away' is a story about not knowing.

Describe a time you had to collaborate with non-engineering stakeholders to deliver a feature.

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Close with what you would do differently, concretely.
  3. State the situation in two sentences and spend the rest on the reasoning.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?

Tell us about your experience working on various projects.

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Name the disagreement and how you resolved it with evidence.
  3. State the situation in two sentences and spend the rest on the reasoning.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?

Estimate a claims reprocessing backfill you have never run

Medium · estimation · backfill · throughput · accumulators

Contract reference data was corrected and 14 months of claim lines must be reprocessed so they reach the answers the pipeline would give today. Nobody on the team has run a backfill at this size. Benefit application reads and writes a per-member accumulator, so the result for a line depends on which lines were applied before it. Finance wants an estimate this week and will plan around it. Describe how you produced one: what you measured instead of guessing, the assumption that dominated the range, and how you communicated the uncertainty without refusing to commit.

Approach

  1. The probe is whether you convert unknowns into measurements inside the week rather than either guessing or stalling. Take the three you can measure now: time one claim line end to end in isolation, count the lines in scope with one query against claim_line restricted to the surviving version per claim, and find where the per-line time actually goes — dependency calls, reference lookups or the accumulator write.
  2. Find the ceiling, which here is not CPU. Because benefit application is a read-modify-write on a per-member accumulator and the outcome depends on application order, lines for one member must be processed sequentially in service-date order. Throughput therefore scales with the number of distinct members in flight, not with the number of lines, so member cardinality is a required input to any number you give.
  3. Measure the tail, not the average. A batch's wall-clock duration is set by the slow end of the per-line latency distribution and by retries against the slowest dependency, so record p50 and p99 and build the estimate on the one that governs.
  4. Express the estimate as a product of measured terms with an explicit range on each, mark the widest term, and design the single spike that collapses it — normally a dry run of ten thousand lines against a copy, timed, which turns two of the terms into constants.
  5. Price the parts nobody asked about but everyone will need: what happens to remittance already issued when a paid amount changes, whether reprocessing writes new claim versions or mutates rows, and whether the run is restartable from the middle after a failure at hour nine.
  6. Commit to something checkable rather than to a number you will defend later: a date for the spike and a revised estimate after it, with the current range and its assumptions written where the planning happens.

Follow-up

  • Your dry run comes in four times slower than the isolated per-line measurement. What are the two most likely reasons, and which do you check first?
  • Reprocessing changes a member's accumulator mid-year. What does that do to claims already adjudicated after the ones you are reprocessing?
  • Finance asks for a single date. What do you actually say?