Interview guides · Komodo Health
Komodo Health Software Engineer Interview Questions & Guide 2026
Code reading practice for Komodo Health software engineer prep: work out what unfamiliar code does before judging it.
The 60-second brief
As a Software Engineer at Komodo Health, you build and scale the underlying technology powering a major healthcare intelligence platform. Your code and system designs directly support the mission to reduce the burden of disease by mapping the complete patient journey. You will work on distributed systems, data pipelines, backend services, or frontend applications that process massive volumes of healthcare data to deliver actionable insights for life sciences and healthcare organizations.
For a design discussion, a catalogue of architectures is worth less than the ability to turn a vague requirement into a data model and an API contract. A box diagram with no schema under it collapses at the first follow-up question.
Focus areas
- Design person merges that remain reversible afterwards
- Hold accumulator limits under concurrent claim adjudication
- Authorise every identified read against current consent
Overview
This position sits at the intersection of heavy data scale, complex domain logic, and modern product engineering. You will collaborate closely with product managers, data scientists, and engineering peers to design robust APIs, optimize database schemas, and ship features that make healthcare data more transparent and accessible. The role demands both technical depth in modern software stacks—such as Python, SQL, and distributed infrastructure—and a strong pragmatic focus on building maintainable, high-performance systems.
You can expect an environment that values technical clarity, practical problem-solving, and cross-functional collaboration. While the scope of work is ambitious, interviewers and teams prioritize direct, real-world engineering capability over abstract brain teasers. Success in this role requires you to balance architecture-level thinking with disciplined execution, ensuring that every service you touch remains resilient and scalable.
The Komodo Health interview process
Komodo Health candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Initial Screening
The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.
What to demonstrate
- Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
- Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
- Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year
How to prepare
- Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
- Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
- Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter
Technical Interviews
Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Final 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
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.
Upserting on the order or result identifier, so a correction overwrites the original row.
It makes the question 'what did the clinician see at 14:02' unanswerable, which is exactly what an incident review or a legal hold asks. It also leaves downstream consumers that already acted on the preliminary value with no correction event to react to, because the state transition was collapsed into a single mutated row and never emitted.
Saying 'eventually consistent' without naming the anomaly a user would see
Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.
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.
Questions & practice
Given an example code base, modify a feature or fix a bug while preserving existing unit test coverage.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Restate the input: its shape, its size, and what is guaranteed about it.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Implement a rate limiter to protect public-facing API endpoints from abuse.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- What is the worst case, and how likely is it on real data?
- Which test case would catch an off-by-one here?
Solve a coding challenge involving array manipulation or string parsing (typically LeetCode easy to medium difficulty).
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- How does this change if the input no longer fits in memory?
- What is the worst case, and how likely is it on real data?
Write a recursive function to traverse and evaluate a complex permission tree.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Sum surviving claim versions in one pass over unordered lines
Easy · hash map · versioning · streaming aggregation
You are streamed up to 50 million claim_line records in arbitrary order: claim_id, claim_version, line_number, frequency_code (original, replacement, void), enterprise_person_id, allowed_amount_cents. Every version of a claim shares its claim_id, and a replacement arrives as a higher claim_version. Return total allowed_amount_cents per enterprise_person_id, counting each claim once at its highest version and contributing zero when that version is a void. Amounts are non-negative int64 minor units. Target O(N) time and O(C) space for C distinct claims, single pass, no sort.
Approach
- Say the two grains out loud before writing anything: the version lives at claim grain, the money lives at line grain. Every wrong answer here comes from applying a claim-level rule with a line-level filter.
- Keep one hash map from claim_id to a small record (best_version, sum_cents, person_id, is_void). Per line: if claim_version is greater than best_version, reset sum_cents to this line's amount and overwrite the person and void flag; if equal, add to it; if lower, discard the line. That reset is what makes the pass order-independent, so a replacement arriving before its original still wins.
- Resolve the void at the end, not at ingest. A void only nullifies the claim if it is the surviving version, and zeroing on sight would let an earlier replacement that arrives later resurrect the money.
- Fold the C claim records into a person-keyed map in a second phase, O(C). Do not accumulate into the person total during the stream: you cannot subtract a superseded version you have already forgotten.
- Cost is O(N) time, O(C) space. The sort-based alternative, group by (claim_id, claim_version) and keep the max, is O(N log N) and needs the set resident; prefer it only when C approaches N and the map will not fit.
- Keep cents in int64 throughout. Fifty million lines at a realistic per-line ceiling stay four orders of magnitude below 2^63, so the integer type costs nothing and removes the float drift class entirely.
Worked solution (20 min)
- Define the per-claim record and the three-way comparison on claim_version: greater resets, equal accumulates, lower discards.
- Run the stream once, touching only the claim map.
- Walk the claim map, skipping entries whose surviving version is a void, and add each remaining sum into a person map.
- Return the person map sorted descending by total.
Expected result: Claim A: version 1 has lines of 12000 and 8000 cents, version 2 (replacement) has lines of 9000, 8000 and 500. Claim B: version 1 has a single line of 5000, version 2 is a void carrying one zero-amount line. Both claims belong to one person. Your answer is 17500 cents, from claim A's version 2 only. The naive sum over all seven lines is 42500.
Verify it yourself
- The number of claims contributing equals the number of distinct claim_ids whose highest version is not a void.
- For a multi-line claim, the addends in its sum equal that surviving version's line count, not one.
- Shuffling the input and re-running produces an identical result, which is the property the reset-on-higher-version rule buys you.
- The all-rows sum is greater than or equal to your total, equal only when no claim has a version above 1 and none is a void.
Follow-up
- You shard the stream across eight workers. Sharding by hash of claim_id works; what exactly breaks if you shard by enterprise_person_id instead?
- A replacement arrives for a claim_id whose original never appears in the batch. What does your map produce, and is that the financially correct answer or a reconciliation break?
- The same claim_id is reused by two different submitting organisations. How does the key have to change, and when would you have noticed?
Return the displayable version of each analyte with a window function
Medium · window functions · versioned rows · indexing
observation_result is append-only: observation_id, enterprise_person_id, encounter_id, accession_id, placer_order_id, filler_order_id, loinc_code, value_numeric, unit_ucum, result_status in ('registered','preliminary','final','amended','corrected','entered_in_error'), collected_ts, issued_ts, version, supersedes_observation_id. A chart view needs the currently displayable value for each (accession_id, loinc_code) for one person over the last 90 days, excluding analytes whose newest version is entered_in_error but keeping analytes whose older versions were. Write the query, give the index that serves it, and say honestly which part of the plan the index cannot remove.
Approach
- Recognise the shape: this is a top-1-per-group over a version chain, not a filter. Pick the newest version first, then apply the status predicate to the winner — the order is the whole exercise.
- Use DISTINCT ON (accession_id, loinc_code) with ORDER BY accession_id, loinc_code, version DESC, observation_id DESC on PostgreSQL; the ORDER BY must start with the DISTINCT ON expressions or it is a syntax error. Use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY version DESC, observation_id DESC) with an outer WHERE rn = 1 if you need portability, since a window function cannot be referenced in the WHERE of its own query level.
- Push only person and time into the inner WHERE. Pushing result_status <> 'entered_in_error' inside promotes a retracted result's predecessor back onto the chart, which is the bug the prompt is testing for.
- Index (enterprise_person_id, collected_ts DESC) so the 90-day slice is an index range scan rather than a scan of the person's whole history.
- Be straight about the limit: because collected_ts carries a range predicate, no b-tree index can also deliver rows pre-ordered by (accession_id, loinc_code, version DESC), so the sort stays in the plan. That is acceptable because one person's 90-day slice is hundreds to low thousands of rows, and it is a quicksort in work_mem rather than an external merge — verify that rather than assume it.
Worked solution (30 min)
- Write the DISTINCT ON form with the full sort key and only person plus time in the inner WHERE.
- Wrap it in an outer query and apply the entered_in_error filter there, so it can only eliminate winners.
- Write the ROW_NUMBER equivalent and confirm both return identical rows against a fixture containing one preliminary, one final, one corrected and one retracted chain.
- Add the index, then read EXPLAIN (ANALYZE, BUFFERS) and confirm an index scan feeding a Sort node, with Sort Method reported as quicksort and no Disk usage.
- Construct the adversarial fixture: an analyte whose version 1 is entered_in_error and whose version 2 is final. It must appear in the output; an analyte whose version 2 is entered_in_error must not.
Expected result: SELECT * FROM ( SELECT DISTINCT ON (o.accession_id, o.loinc_code) o.accession_id, o.loinc_code, o.value_numeric, o.unit_ucum, o.result_status, o.collected_ts, o.issued_ts FROM observation_result o WHERE o.enterprise_person_id = :p AND o.collected_ts >= now() - interval '90 days' ORDER BY o.accession_id, o.loinc_code, o.version DESC, o.observation_id DESC ) latest WHERE latest.result_status <> 'entered_in_error'; with CREATE INDEX ON observation_result (enterprise_person_id, collected_ts DESC). The plan is an index scan on that index feeding a Sort feeding Unique; the sort cannot be indexed away because collected_ts carries a range predicate.
Verify it yourself
- Output row count equals SELECT count(DISTINCT (accession_id, loinc_code)) over the same window minus the number of chains whose newest version is entered_in_error.
- Every returned observation_id has no other row pointing at it via supersedes_observation_id within the window.
- All versions of one analyte share the same collected_ts; if they do not, the 90-day window can cut a version chain in half and return a stale winner.
- EXPLAIN (ANALYZE) reports Sort Method: quicksort with Memory in kilobytes, not external merge Disk — if it spills, the per-person slice is larger than assumed and the design needs revisiting.
Follow-up
- A correction arrives carrying the same filler_order_id and the same version number as the row it corrects. What breaks, and what do you add to the sort key?
- The same query for a cohort of 50,000 people instead of one person: does DISTINCT ON still hold up, and at what point do you materialise a latest-version table with a partial index?
- How would you show the clinician that the displayed value was amended, given the chart query returns only the winner?
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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?
Walk through your approach to designing a secure API that complies with strict healthcare data privacy standards.
Medium · system design and architecture
Approach
- Name the failure you are designing for, then the recovery path.
- 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
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
Design a real-time data ingestion pipeline for streaming healthcare claims data.
Medium · system design and architecture
Approach
- Choose a partition key and say what query it makes expensive.
- 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.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
Write a function to parse and transform a nested JSON payload representing patient encounter records.
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Design an error taxonomy for a real-time eligibility check
Easy · error taxonomy · timeouts · graceful degradation
The registration desk calls GET /eligibility?person_id=&plan_id=&service_date= before a visit. The service answers from locally held coverage_span rows or by an external inquiry that takes seconds and sometimes fails outright. Staff must then proceed, proceed with an unverified flag, or stop, and the screen cannot make that call on its own. Define the response and error taxonomy: which outcomes are 2xx with a payload, which are 4xx, which are 5xx, what each carries, which are safe to retry, and what the desk is instructed to do for each. State the timeout budget.
Approach
- Separate the three things a boolean conflates: covered, not covered, and could not determine. Only the first two are answers. Flattening the third into 'not covered' produces a wrong financial answer wearing the clothes of a determinate one, and the desk cannot tell the difference.
- Put every determinate and indeterminate answer on 200 with a discriminated payload: {determination: covered | not_covered | indeterminate, reason_code, as_of, computed_at, source: local | payer | cache, coverage_order, cost_share, next_action}. The transport status describes the call; the payload describes the coverage. A not_covered answer is a successful call.
- Reserve 4xx for caller faults a retry cannot fix and make them distinguishable, because the remedy differs: 404 unknown person, 422 malformed or out-of-range service_date, 403 when the access decision refuses the purpose of use. Nothing retryable belongs in 4xx.
- Treat an external timeout as an outcome, not a transport failure. Return 200 with determination=indeterminate, reason_code=payer_timeout, and the locally derived coverage_span answer as of service_date alongside it. A 504 here throws away a usable degraded answer the desk could act on. Keep 5xx for the case where even the degraded answer could not be produced.
- Fix the budget end to end and state who enforces it: a roughly 2s desk interaction budget, a hard 1500ms deadline on the external inquiry, the remainder for the local path, and a client timeout set strictly above the server's so the server decides the fallback rather than the client abandoning a call that was about to degrade correctly. Note that every outcome here is retry-safe because the call is a read whose only side effect is the access audit row.
Worked solution (20 min)
- Write the full payload for a covered answer, including as_of, computed_at and the coverage_order that drives coordination of benefits.
- Enumerate every failure the call can hit - unknown person, bad date, consent refusal, no local data, payer timeout, payer error, our own fault - and assign each a status code and a reason code.
- For each row write the one sentence the desk sees and the action offered, then check that no two causes collapse into the same action.
- Do the deadline arithmetic: total budget, external deadline, remaining local budget, and the client timeout that must exceed the server's.
- State the retry rule per class: retryable, not retryable, or retryable with backoff only.
Expected result: 200 for covered, not_covered and indeterminate, each carrying {determination, reason_code, as_of, computed_at, source, next_action}; 404 unknown person; 422 malformed or out-of-range service_date; 403 consent or purpose-of-use refusal; 502 or 504 only when not even the degraded local answer could be produced; 500 for our own faults. A payer timeout is 200 + indeterminate + payer_timeout, carrying the local coverage_span answer as of service_date. External deadline 1500ms inside a 2s desk budget, with the client timeout set above it.
Verify it yourself
- No single field doubles as both the transport result and the coverage answer.
- Every 4xx names an input the caller can change, and nothing retryable is a 4xx.
- An indeterminate answer still carries an as_of date and whatever local coverage was readable.
- The client timeout is strictly greater than the server's external deadline, so degradation is the server's decision.
Follow-up
- Two payers answer and disagree about which is primary. What does the endpoint return?
- How long may an eligibility answer be cached, and what exactly is the cache key?
- The desk proceeded on an indeterminate answer and the claim denies six weeks later. What in the response makes that decision reconstructable?
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 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.
The bug you decided not to fix
Engineers over-index on what they repaired. A stronger answer covers something you knowingly left broken: the alert you tuned down, the data inconsistency you documented instead of chasing, the cleanup you deferred past two quarters. Give the reasoning and the condition that would have reopened it, so it reads as a decision and not as neglect.
Share an example of a time when a production deployment failed and how you managed the incident and post-mortem.
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
- 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?
How do you approach code reviews and maintain quality standards within a fast-paced engineering team?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Explain a wrong lab value without absorbing another team's defect
Easy · debugging · cross-team · communication · replica lag
A clinician reports a potassium result on the chart that does not match what the performing laboratory's own system shows. The value crossed a partner interface, the gateway's normalisation, observation_result, a cache and a read replica, and the laboratory is a separate organisation you cannot instrument. The first message in the channel says the feed must be wrong. Describe handling a discrepancy of this shape: what you could prove from your side alone, what you asked the other party for and why, and how you explained the cause in two minutes to someone who would not accept 'upstream' without evidence.
Approach
- The probe is whether you localise before you theorise. Fix the observable first as a row: enterprise_person_id, accession_id, loinc_code, the displayed value and unit, and the minute it was displayed. 'A potassium was wrong' is not yet a question that any query can answer.
- Build a five-layer table — source, transformation, store, cache and replica, serving — and put exactly one decisive query or log line in each row, so the investigation is a sequence of eliminations rather than a discussion.
- Run the store row first, because it is one query and closes a large share of these: every version for that accession and analyte with version, issued_ts, result_status and unit_ucum, ordered by version. A corrected version issued before the display minute means nothing upstream is wrong at all.
- Check unit_ucum explicitly at the transformation layer. A value that is numerically correct in one unit and wrong in the displayed one is a conversion defect in your gateway, not a feed defect, and it is the failure that most often gets misattributed outward.
- Rank the remaining checks by cost. Replica lag and cache age at that minute are cheap and are ordinary explanations; asking another organisation for their audit trail takes days, so it goes last and it must name the artefact — the message as transmitted, with its timestamp — because their current screen answers a different question than the one you are asking.
- Rehearse the two-minute version and cut it until every sentence carries either evidence or the fix: the value, the layer, the proof, the change. Anything the listener would have to take on trust is the part that will be challenged.
Follow-up
- The archived raw message matches what the laboratory sent, and your stored value differs. Where do you look next, and in what order?
- How do you carry a correlation identifier across a batch file and a legacy message feed that have no place to put one?
- The clinician asks whether other results are affected. What query answers that, and how confident is the answer?