Interview guides · Dozee
Dozee Software Engineer Interview Questions & Guide 2026
Take-home style practice for Dozee software engineer candidates: a small service, a fixed budget, a written tradeoff.
The 60-second brief
A Software Engineer at Dozee plays a pivotal role in bridging the gap between cutting-edge medical technology and scalable software solutions. As the company continues to innovate in the remote patient monitoring space, your work directly impacts the reliability and accuracy of clinical data, ultimately influencing patient outcomes. You are not just writing code; you are building the infrastructure that allows healthcare providers to receive real-time, life-saving insights.
Getting the code to run is the floor. What usually separates answers is the case checked without prompting: empty input, a single element, duplicate keys, or a value that overflows the integer type you chose.
Focus areas
- Authorise every identified read against current consent
- Make HL7 and X12 ingestion idempotent under replay
- Model bitemporal coverage and retroactive eligibility changes
Overview
This role is inherently collaborative and requires a balance of technical rigor and a startup-oriented mindset. Because Dozee operates in the high-stakes environment of health-tech, you will be expected to handle complex data streams with precision. You will work within a fast-paced team environment, contributing to the evolution of a platform that is redefining how medical monitoring is approached globally.
Where to focus your preparation
Treat the sections below as preparation areas and confirm the format with your recruiter.
Preparation focus
No round sequence has been reported for this company, so work the categories below and confirm the format with your recruiter.
What to demonstrate
- Breadth across SQL, experimentation and product reasoning
- Ability to state assumptions before choosing a method
How to prepare
- Drill the practice exercises below and time yourself
- Prepare three quantified stories about decisions you drove
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.
Storing monetary amounts as floating-point numbers.
Binary floating point cannot represent values like 0.10 exactly, so summing allowed and paid amounts across millions of claim lines accumulates representation and rounding error. Reconciliation against a remittance file is exact to the cent, so the drift surfaces as a balance that is off by a few cents with no identifiable cause, and engineers lose days looking for a logic bug in code that is correct apart from its numeric type. Integer minor units or an exact decimal type removes the class entirely.
Trusting input because it came from your own front end
Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.
A queue or buffer with no bound
Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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).
- 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.
- 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.
- 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?
Group transfer-chained encounters into episodes with out-of-order arrival
Medium · graph traversal · cycle detection · memoisation
You have up to 5 million encounter rows for a reporting month, in arbitrary order: encounter_id, enterprise_person_id, facility_id, encounter_class, admit_ts, discharge_ts (nullable), status, prior_encounter_id (nullable, set on transfer), source_event_ts. prior_encounter_id may reference a row that arrives later or never arrives. Group rows into episodes, one per maximal transfer chain, returning episode root, person, earliest admit_ts, latest discharge_ts (null if any member is still open) and member count. Report cycles instead of following them. Target O(N) expected time.
Approach
- Recognise the shape: every row has at most one prior_encounter_id, so this is a forest of parent pointers, not a general graph. A hash map from encounter_id to row is the whole index you need, and the work is O(N) expected rather than anything sort-shaped.
- Resolve roots by iterative pointer-following with write-back memoisation: from each node walk prior_encounter_id until you reach a node with no parent, a parent absent from the input, or a node whose root is already known, then stamp the discovered root onto every node on that path. Each node is finalised once, so total work stays O(N) even when one chain is very long.
- Do not recurse. A malformed feed can produce a chain tens of thousands deep, and a recursive resolver dies on stack depth taking the whole batch with it. An explicit loop with write-back is both faster and bounded.
- Detect cycles with three-state marking: unvisited, on the current path, finalised. On reaching a node that is on the current path, emit a data-quality record naming every encounter_id in the cycle and exclude that component from the episode output. Choosing an arbitrary root instead would produce an episode that looks plausible and will be believed.
- Keep a dangling prior_encounter_id distinct from a genuine root. It means the chain starts outside the window or has not arrived, so flag the episode left-truncated and carry that flag into the output: an episode with an unknown start cannot be used for length of stay or readmission counting without biasing both downward.
- Compute aggregates during the same pass that assigns roots: minimum admit_ts, maximum discharge_ts with null propagation so any open member leaves the episode open, and exclude cancelled and entered_in_error members from the aggregates while keeping them retrievable.
Worked solution (25 min)
- Load all rows into a hash map keyed by encounter_id, with a colour array alongside.
- For each unfinalised node, walk parents onto an explicit path stack until you hit a root, a dangling reference, a finalised node or an on-path node.
- Stamp the resolved root, or the cycle marker, back onto every node on the path.
- Fold each node into its root's aggregate: min admit_ts, max discharge_ts with null propagation, member count, left-truncated flag.
- Emit episodes and the cycle records as two separate outputs.
Expected result: Rows arriving as E3 (prior E2), E1 (prior null), E2 (prior E1) form one episode rooted at E1 with three members, admit from E1 and discharge from E3. A row E9 whose prior E8 never arrives forms a one-member episode flagged left-truncated, and its admit_ts is not the episode's true admission. A pair E5 and E6 pointing at each other yields no episode and one cycle record naming both.
Verify it yourself
- Episode member counts plus cycle members sum to N exactly.
- Every episode has exactly one member whose prior_encounter_id is null or dangling.
- Shuffling the input produces identical episodes: same roots, same member counts, same aggregates.
- No episode has its earliest admit_ts after its latest discharge_ts. When one does, comparing facility_id on those two rows separates a wrongly-built chain from genuine clock skew between facilities.
Follow-up
- Two encounters name each other as prior_encounter_id. What does your detector emit, and who needs to see it?
- One episode spans two facilities whose clocks differ by four seconds. Do you order on source_event_ts or admit_ts, and what does the choice cost?
- A chain is cut in half by the month boundary. What does this month's report say, and how do you make next month's agree with it?
Bitemporal coverage table with retroactive termination queries
Easy · bitemporal · schema design · retroactive correction
You maintain coverage_span: coverage_id, enterprise_person_id, payer_id, plan_id, effective_date, termination_date (nullable, meaning open-ended, not unknown), status, valid_from timestamptz, valid_to timestamptz DEFAULT 'infinity'. An enrolment file received on 2026-03-05 terminates a member effective 2026-03-01, after a claim for service date 2026-03-03 was already adjudicated against the answer then in force. State your termination_date convention, write the two SELECTs — (a) is the person covered on 2026-03-03 as known now, (b) what the system believed on 2026-03-04 — and say precisely what the loader writes when the file lands.
Approach
- Separate the two time axes explicitly before writing anything: effective_date/termination_date are business time (when benefit applied), valid_from/valid_to are system time (when this row was the system's belief). Every query names both, or it is ambiguous.
- Pin the inclusivity convention, because the predicate depends on it. Enrolment files conventionally carry termination_date as the last covered day, so the business-time predicate is effective_date <= :svc AND (termination_date IS NULL OR termination_date >= :svc). If your feed uses an exclusive end, it becomes > :svc, and a half-open daterange column removes the ambiguity permanently.
- Write the as-known-now query by pinning system time to the open version (valid_to = 'infinity') and letting business time float to the service date.
- Write the as-of-belief query by pinning system time to an instant: valid_from <= :asof AND valid_to > :asof. Half-open on both ends is what makes 'exactly one row per key per instant' provable.
- Describe the write as one guarded statement rather than two independent ones: a data-modifying CTE whose UPDATE stamps the open row's valid_to to now() (the only UPDATE the design permits — it stamps system time, it never edits business facts) and returns that row's keys, with the INSERT of the new version selecting FROM that CTE. Feeding the INSERT from the UPDATE's RETURNING is what makes a replayed file a no-op; an unconditional INSERT leaves the partial unique index to raise 23505 instead, which is a backstop rather than idempotency. The pre-correction row stays readable either way, which is what makes the already-adjudicated claim defensible.
Worked solution (25 min)
- Write the DDL with both ranges half-open in system time: valid_from timestamptz NOT NULL, valid_to timestamptz NOT NULL DEFAULT 'infinity', and a partial unique index on (enterprise_person_id, plan_id, effective_date) WHERE valid_to = 'infinity' so only one belief is open per business period.
- Write query (a) against the open system-time version, with the business-time predicate on the service date.
- Write query (b) by replacing valid_to = 'infinity' with the half-open instant predicate, keeping the identical business-time predicate so the only thing that changed is which belief you read.
- Write the loader as one statement: WITH closed AS (UPDATE ... RETURNING ...) feeding INSERT ... SELECT FROM closed. Guard the UPDATE with valid_to = 'infinity' so a concurrent loader cannot close the same row twice, and with a termination_date IS DISTINCT FROM :new_term test so a replay of the same file matches no row and therefore inserts none. Under READ COMMITTED a second loader that blocks on the row lock re-evaluates the guard against the newly committed version, fails it, and writes nothing.
- Trace the 2026-03-05 file by hand: before it, one row (effective 2026-01-01, termination NULL, valid_to infinity); after it, two rows, and confirm (a) returns not-covered while (b) returns covered.
Expected result: (a) SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = :p AND valid_to = 'infinity' AND status = 'active' AND effective_date <= DATE '2026-03-03' AND (termination_date IS NULL OR termination_date >= DATE '2026-03-03'); returns no row. (b) the same query with valid_to = 'infinity' replaced by valid_from <= TIMESTAMPTZ '2026-03-04 00:00+00' AND valid_to > TIMESTAMPTZ '2026-03-04 00:00+00'; returns the pre-correction row with termination_date NULL. The loader runs as one statement: WITH closed AS ( UPDATE coverage_span SET valid_to = now() WHERE enterprise_person_id = :p AND plan_id = :pl AND effective_date = DATE '2026-01-01' AND valid_to = 'infinity' AND (termination_date IS DISTINCT FROM DATE '2026-03-01' OR status IS DISTINCT FROM 'terminated') RETURNING enterprise_person_id, payer_id, plan_id, effective_date ) INSERT INTO coverage_span (enterprise_person_id, payer_id, plan_id, effective_date, termination_date, status, valid_from, valid_to, source_file_id) SELECT enterprise_person_id, payer_id, plan_id, effective_date, DATE '2026-03-01', 'terminated', now(), 'infinity', :f FROM closed;
Verify it yourself
- For any fixed instant, the system-time predicate selects exactly one row per (person, plan, business period): SELECT enterprise_person_id, plan_id, effective_date, count() FROM coverage_span WHERE valid_from <= :t AND valid_to > :t GROUP BY 1,2,3 HAVING count() > 1 returns zero rows.
- Query (b) with :asof = now() returns exactly what query (a) returns — if it does not, the close-and-insert is not atomic or the half-open boundaries overlap.
- No row's valid_from equals its valid_to, and no two versions of one coverage_id have overlapping [valid_from, valid_to).
- Replaying the same enrolment file inserts nothing and raises nothing: the guarded UPDATE matches no row because the open version already carries termination_date 2026-03-01 and status 'terminated', and the INSERT selects FROM that UPDATE's RETURNING, so it has zero input rows. Assert the person's row count is unchanged. A loader that instead runs the INSERT unconditionally fails the replay with SQLSTATE 23505 from the partial unique index — that stops the duplicate, but it is an error the caller must handle, not an idempotent replay.
Follow-up
- The claim adjudicated on 2026-03-03 is now retroactively uncovered. What does reprocessing have to read — the belief at adjudication time or the current truth — and what does the remittance reversal reference?
- Add the constraint that stops two active coverage versions from overlapping in business time for the same person and plan, without blocking the superseded history rows.
- A later file reinstates coverage with the same effective_date. How many rows exist for that period afterwards, and which one does query (a) return?
Denormalise consent into a decision table with a stated staleness bound
Hard · denormalisation · cache invalidation · access control
Access governance answers 'may this requester read this person's data for this purpose' on every identified read — tens of thousands per second at a 5ms p99 — from consent_directive: consent_id, enterprise_person_id, version, scope, purpose_of_use text[], grantee_org_id (NULL meaning all organisations), permit boolean, effective_ts, expires_ts, revoked_ts, status. Evaluating the normalised version history per read misses the budget. Design the denormalised decision store and its key, state the revocation staleness bound in seconds and the mechanism that actually enforces it, and say which normalised rows remain the system of record and why.
Approach
- Derive the shape from the read, not from the source table. The read is a point lookup on (person, grantee organisation, purpose), so fan the purpose_of_use array out into one row per purpose and make those three columns the primary key. A GIN index on the array would serve containment queries nobody issues.
- Handle the wildcard concretely. grantee_org_id NULL means 'all organisations', and NULL never equals anything, so a nullable column in the key breaks both the primary key and the equality lookup. Store a sentinel org id of 0 for the wildcard and resolve specific-before-wildcard in the lookup, or the wildcard row is silently unreachable.
- Make refusal a row, not an absence. An explicit permit = false must outrank a permit for the same key, so carry a precedence rank (specific grantee beats wildcard, refusal beats permit at equal specificity) and resolve with ORDER BY rank LIMIT 1. Default deny when nothing matches, so a rebuild failure fails closed.
- Separate the two staleness mechanisms and be precise about which one is the bound. The in-process decision cache has a TTL; a revocation also publishes an invalidation. The publish shortens the typical case to milliseconds, but it can be lost, so the guaranteed bound is the TTL alone — quote that number, for example 30 seconds, and justify it against the cache hit rate you need to hold 5ms p99.
- Evaluate expires_ts at read time against the request clock rather than baking the decision. A cached permit whose expires_ts has passed is wrong regardless of invalidation, because no event fires when a timestamp simply goes by.
- Keep consent_directive as the system of record: audit must reconstruct the directive in force at any past instant, the decision table holds only current state, and a corrupted decision table must be rebuildable by replaying the directives. Bound the write amplification by keeping grantees at organisation granularity and purposes a closed code set, so one directive expands to a known small number of rows rather than an unbounded cross product.
Follow-up
- Break-glass must always succeed and must be unmistakably marked. Where does it sit relative to this lookup, and what stops it from becoming the quiet default path?
- The invalidation bus is down for thirty minutes. Walk through what a revoked person's data exposure looks like minute by minute, and what the backlog does when the bus returns.
- Where does the access audit record get written so that an audit-store outage degrades neither the log's completeness nor the read's availability?
How would you design a system that handles high-frequency data from medical sensors?
Medium · system design and architecture
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the read and write paths separately; they rarely have the same bottleneck.
- State the consistency you need, and where you are willing to be stale.
Follow-up
- What would you drop to keep the system up under load?
- What breaks first when traffic grows ten times?
How do you ensure your code is scalable and performant in a production environment?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Can you describe your process for debugging a complex system issue?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
How do you approach writing unit tests for critical system components?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Benefit accumulators under concurrent claim adjudication
Medium · isolation · lock-contention · partitioning · retries
Adjudication applies benefit by reading a member's remaining deductible and out-of-pocket accumulators, computing patient responsibility, then writing both the new accumulator values and the claim_line amounts (allowed_amount_cents, paid_amount_cents, patient_responsibility_cents, all bigint minor units). Several lines for one member adjudicate concurrently out of a batch of a few million lines a day, alongside a real-time path. On PostgreSQL at READ COMMITTED, exhibit the interleaving that overshoots the plan limit, then give three mechanisms that prevent it and the throughput each one costs.
Approach
- Exhibit the anomaly with numbers. Transactions A and B each SELECT remaining_deductible = 40000 cents, each compute an application of 30000, each UPDATE to 10000, and both commit: 60000 applied against a 40000 limit. The lost update lives across the statement boundary — READ COMMITTED does re-evaluate a blocking UPDATE's own predicate against the newly committed row, but it does nothing about a value the application read in an earlier statement.
- Mechanism one, SELECT ... FOR UPDATE on the accumulator row at the top of the transaction: correct, simple, and it serialises that one member's lines. The cost is a held row lock for the transaction's lifetime, so contract and pricing lookups must complete before the lock is taken — a lock held across a call to an external system is how one slow partner stalls a queue.
- Mechanism two, SERIALIZABLE with a bounded retry on SQLSTATE 40001: no explicit locking, but the abort rate climbs with contention and each retry redoes the whole computation, so effective throughput falls as contention rises rather than degrading gently. The retry is only safe if the transaction is idempotent or can recognise its own partial work by a claim-version key.
- Mechanism three, partition the work queue by enterprise_person_id so one member is handled by exactly one writer: contention disappears instead of being resolved, at the cost of a rebalancing story and a real-time request queueing behind the batch for a hot member.
- Handle reversals, where all three are equally exposed: the amount to subtract is what was applied at the time under the plan design then in force, not what recomputation today would produce. Record the applied amount on the claim version and subtract that stored value.
- Choose and justify for this shape: batch dominates and members are naturally independent, so partition by member for the batch path while the real-time path takes FOR UPDATE on the same row, since both must respect one counter.
Worked solution (30 min)
- Open two psql sessions and reproduce the lost update against a real accumulator row, recording the exact statement order.
- Rewrite the path three ways — FOR UPDATE, SERIALIZABLE with retry, per-member partition — and run the same two-session interleaving against each.
- Measure or estimate throughput per member for each variant and write one paragraph on where each degrades.
- Write the reversal path and state which stored value it subtracts and why recomputation is wrong.
- Mark every point in the transaction where an external call would extend a held lock.
Expected result: A concrete lost-update interleaving under READ COMMITTED, three mechanisms with their distinct costs — held-lock duration, retry amplification under contention, per-member throughput ceiling — a reversal that subtracts the recorded applied amount rather than a recomputed one, and a stated choice with its reasoning.
Verify it yourself
- The lost update reproduces in two real sessions, not only on paper.
- Under each of the three mechanisms, a hundred concurrent applications against a 40000-cent limit leave the accumulator at exactly 40000 and never above.
- A retried transaction under SERIALIZABLE applies the benefit once, verified by the claim version rather than by the accumulator alone.
- No transaction in any variant holds a lock across a network call.
Follow-up
- Eligibility changes retroactively, so a claim adjudicated in March should have denied. What must be recomputed, in what order, and what happens to the accumulators in between?
- A family deductible accumulates across several members' claims simultaneously. What is the lock now, and where is the new deadlock?
One facility stops updating while aggregate ingestion lag looks fine
Medium · poison message · head-of-line blocking · dead-letter queue
Encounters from one sending facility stopped updating at 02:10. Aggregate HL7 ingestion throughput looks normal and the consumer group's total lag is up only 4%. One consumer instance is pinned at 100% CPU. The error log shows the same message control ID repeating about 200 times a second with a parse error on a timestamp field. The stream is partitioned by (sending facility, encounter) to preserve per-encounter ordering. Give the ordered diagnosis, the fix, and what you must not do to the message.
Approach
- Look at lag per partition, never the aggregate. With 64 partitions, one fully stuck partition raises total lag by about 1.6% while that facility's encounters stop entirely, so the aggregate metric is structurally blind to head-of-line blocking. The alert that would have caught this is per-partition lag or a committed offset that has not advanced.
- Confirm the loop by grouping errors by offset or control ID. The same identifier repeating at the retry rate, with the partition's committed offset frozen, is a poison message. A growing set of distinct identifiers instead means a systematic parse regression, which points at a dialect change on the partner's side and has a different fix.
- Classify before acting: parse-time or apply-time failure. A parse failure has written nothing and can be parked safely. An apply failure may have committed part of its work, so you must confirm the write path is idempotent against a unique constraint on the idempotency key before you replay or skip anything.
- Fix the mechanism rather than the one message: bounded retries with exponential backoff and a maximum attempt count, then park the raw bytes, partition key and offset to a dead-letter store and advance the offset. Alert on dead-letter depth, because lag will not catch the next one either.
- Handle the ordering consequence explicitly. Silently skipping a message in a per-encounter ordered stream permanently drops a clinical event — a transfer, a discharge, a result release. Mark the encounter key as blocked and reconcile it, rather than letting later messages apply over a gap nobody recorded.
- Then repair the parser. Partial-precision HL7v2 timestamps are legal: 2026, 202609, 20260922 and 20260922143000-0700 are all valid, so the parser must carry a precision rather than assume fourteen digits plus an offset, and must reject with a typed error the dead-letter path can classify.
Follow-up
- What maximum attempt count do you choose, and what does it imply about a transient failure like a downstream store being briefly unavailable, which looks identical to a poison message for the first few attempts?
- After the parser is fixed, how does an operator replay parked messages for that encounter without re-applying the messages that already succeeded behind it?
Suggested practice checklist
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Day 2 — References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Day 3 — Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Day 4 — Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Day 5 — Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Day 6 — Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Day 7 — Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Review and mentoring: what you let through
Counting review comments or mentees proves nothing. The useful version is a specific change you approved with a reservation you stated, or one you blocked and the delay that cost. Say which standard you were holding and why it was worth the friction. A mentoring story needs the thing the other person can now do without you.
Unblock an engineer whose deductible accumulator overshoots
Easy · mentoring · lost update · isolation levels · reproduction
An engineer on your team has a benefit-application path that occasionally leaves a member's accumulated deductible above the plan limit — a handful of times a day, never in tests. Their code SELECTs the remaining deductible, computes patient responsibility in application code, then UPDATEs the accumulator to an absolute value. They have spent three days adding logging and are now asking whether the database is losing writes. Describe unblocking someone in this position: what you did first, what you let them find themselves, and what you left them able to diagnose next time.
Approach
- The probe is whether you teach a method or hand over a patch. Reproduce before explaining: two sessions, both BEGIN, both SELECT the accumulator row, both compute, both UPDATE to an absolute value, both COMMIT. The second overwrites the first and neither errors. Fifteen minutes of that ends three days of logging and, more importantly, ends the theory that the database is at fault.
- Be precise about where the anomaly is and is not, because the imprecise version teaches the wrong lesson. A single UPDATE that does its arithmetic in SQL is safe under READ COMMITTED: the blocked statement re-reads the row after the first commits. The loss comes from computing the new value in application code across the statement boundary, so the UPDATE writes an absolute amount derived from a stale read.
- Lay out three fixes with their costs rather than naming one. SELECT ... FOR UPDATE serialises writers per member and holds the lock for the transaction, so nothing in that transaction may make an external call. SERIALIZABLE with a retry loop on SQLSTATE 40001 requires the operation to be safely retryable. Per-member partitioning removes contention entirely and adds routing and rebalancing.
- Let them pick and defend one. The outcome you want is that they can name the anomaly and the isolation level unprompted next time, not that this particular ticket closes today.
- Leave an artefact behind: the two-session reproduction checked in, so the next person meets the interleaving rather than the symptom, and the reasoning is not stored only in your head.
Follow-up
- A claim is reversed and the accumulator must be credited back. What amount, given the plan design may have changed since?
- They choose SERIALIZABLE. How do they make the retry safe when the transaction also wrote a claim adjudication row?
- How would you have noticed this before a member saw it, and what would that check cost per adjudication?
Reverse a fail-closed eligibility check at patient registration
Hard · reversing a decision · fail-closed · degradation · migration
You designed registration to block when the external eligibility inquiry exceeds its timeout budget, reasoning that an unverified registration becomes a denial weeks later. Months in, the payer's p99 sits near the budget, front desks are queueing, and two teams have built on the guarantee that a completed registration implies verified coverage. You reversed the decision. Walk through it: the evidence that changed your mind, what you deliberately kept fail-closed, how you migrated the teams relying on the old behaviour, and what you would change about how you made the original call.
Approach
- The probe is whether a reversal is backed by measurement or by fatigue. Lead with both costs in their own units: the share of inquiries exceeding the timeout and the wait minutes that produced at the desk, against the write-off dollars attributable specifically to registrations that proceeded unverified during the pilot.
- State what your original reasoning got wrong, concretely. The usual answer is that you priced the denial and not the queue, and that you reasoned about a p50 round trip when a fail-closed default is governed by the dependency's p99 and by its outage minutes, neither of which you had measured before choosing.
- Show the reversal was partial, which is what a good one looks like. Emergency presentations never block, because care is not conditioned on a benefits answer; high-cost elective services keep a hard stop where the write-off is large and the delay is tolerable; everything else proceeds with coverage_status 'unverified' into a work queue that must clear before the claim is submitted.
- Treat the work queue as part of the design rather than as an afterthought: what enters it, what clears it, what happens when it is not cleared before the submission deadline, and who is paged when its depth grows. A fail-open path with no queue is not a degradation strategy, it is a silent one.
- Describe the migration for the teams that depended on the guarantee: a per-encounter_class flag, a period where the new decision is computed and logged while the old behaviour still stands, and the consumers told which field replaces the assumption before the switch, not after.
- Close on the process change rather than the outcome — what you would now require before shipping any fail-closed default: a measured p99 from the dependency, a named owner for the blocked case, and a reversal trigger agreed in advance so the next reversal is a threshold being crossed rather than an argument.
Follow-up
- Your unverified queue is 900 deep on a Monday and the submission deadline is Wednesday. What happens, and who decided that in advance?
- How do you prevent 'unverified' from quietly becoming the normal path once the desks learn it is faster?
- Six months later the payer's p99 halves. Do you reverse back, and what evidence would make you?
Argue against rewriting foreign keys during a person merge
Hard · merge and unmerge · schema design · technical disagreement · read path
A proposal on your team simplifies merges: on merge, UPDATE enterprise_person_id across encounter, observation_result and claim_line to the surviving identity, then delete the link-resolution step so a patient-scoped read becomes one equality predicate. The author brings a p99 improvement on the patient summary, which fans out across eight to twelve resource types. You argued against it. Reconstruct the argument: the failure you predicted, the evidence you brought, the cost of your own alternative that you conceded, and who made the call.
Approach
- The probe is whether you can oppose a real performance win without retreating to data-hygiene arguments. Name the operation the proposal removes: unmerge. Once enterprise_person_id is rewritten in place, the row no longer records which source identity asserted the fact, and the only pair of columns that could — assigning_authority and source_person_id — lives in the link table the proposal deletes.
- Make it concrete with money rather than with charts. A claim_line posted under the losing identity carries coverage_id pointing at that identity's enrolment span. Rewriting the person while leaving coverage_id yields a row asserting the surviving person was covered under a span that was never theirs, which is a financial statement that adjudication and reconciliation will both act on.
- Do not argue that merges are rare, because that concedes the premise. Bring the rate of merges, the rejection rate on manually reviewed links, and the lag between a wrong merge and its discovery — false merges are a property of probabilistic matching, so the design has to assume them rather than hope against them.
- Concede the read cost honestly, since that is where the proposal's benefit is. Resolution through a versioned link table turns one equality predicate into a lookup feeding a predicate over a set of source identities, which changes index shape and raises the composite read's p99, and the composite is the number that matters, not the per-resource one.
- Offer the option that captures most of the win: a materialised current-link projection refreshed inside the merge transaction, so reads stay one predicate while the versioned link table remains authoritative and unmerge stays a supported operation rather than a recovery exercise.
- Finish on the decision process, not the technical point: what you wrote down, whether you blocked the change, who owned the call, and what you did once it was made — including if it went against you.
Follow-up
- Your projection and the link table disagree after a failed merge retry. Which one is authoritative, and how does a reader find out?
- An unmerge lands eight months later. What has to happen to the claims adjudicated under the merged identity in between?
- The author says reversibility can be handled by a nightly backup restore. Take that seriously and say exactly what it does not recover.