cswork / libraryOpen practice workspace

Interview guides · Doctor Care Anywhere

Doctor Care Anywhere Software Engineer Interview Questions & Guide 2026

Timed coding practice for Doctor Care Anywhere software engineer candidates: full attempts under a clock, then scored against a rubric.

The 60-second brief

As a Software Engineer at Doctor Care Anywhere, you are at the heart of a mission-driven organization dedicated to revolutionizing how patients access healthcare. Your work directly impacts the delivery of high-quality, digital clinical services, ensuring that technology bridges the gap between patients and medical professionals. You will be responsible for building, maintaining, and scaling the platforms that make remote healthcare reliable, secure, and intuitive.

The loop does not sample the job evenly, and arguing about that in the room costs you. Daily work is mostly incremental change inside code someone else wrote, while the loop samples narrow slices of it; prepare for the slices and save the realism argument for your questions at the end.

Focus areas

  • Version clinical results instead of updating rows in place
  • Model bitemporal coverage and retroactive eligibility changes
  • Design person merges that remain reversible afterwards

Overview

This role is both technically demanding and strategically significant. You will be expected to tackle complex problems in a highly regulated environment, balancing rapid feature development with the rigorous standards required for healthcare technology. Whether you are optimizing backend systems or improving the patient-facing interface, your contributions will have a tangible impact on the lives of users who rely on Doctor Care Anywhere for their primary care needs.

The Doctor Care Anywhere interview process

Doctor Care Anywhere candidates report 5 rounds · ≈ 4-6 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 Skills Evaluation

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What to demonstrate

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Collaboration Assessment

What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.

What to demonstrate

  • Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
  • Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
  • Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly

How to prepare

  • Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
  • Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
  • Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live

Engagement with Leadership

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

Feedback Process

When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.

What to demonstrate

  • Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
  • Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
  • Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
  • Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened

How to prepare

  • Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
  • Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
  • Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure

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.

Treating a medical record number or a member ID as a globally unique key and joining on it directly.

These identifiers are unique only within the authority that issued them. Two facilities in one network routinely have the same medical record number for different people, and member IDs get reissued when someone changes plans. Joining on the bare value merges two patients' records, which is the most damaging failure available in this domain, and it passes every test written against a single-facility fixture because the collision only appears once a second source is connected.

Tests that assert on the implementation rather than the behaviour

Assert on what a caller can observe, not on the number of internal calls or the shape of a private field. A test that breaks on every refactor but still passes when the answer is wrong costs more than it protects.

Reading the constraints as preamble rather than as part of the problem

The bounds are usually there to eliminate the obvious approach: n up to 10^5 makes an O(n^2) scan roughly 10^10 operations, far outside any per-test time budget, and an input larger than memory rules out loading it at all. When a bound is not given, ask for it, then say out loud which approach it kills.

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?

Flag a requester reading too many distinct charts per window

Medium · sliding window · two pointers · distinct count

You consume access-audit records (event_ts, requester_id, enterprise_person_id, purpose_of_use, break_glass) at tens of thousands per second, non-decreasing in event_ts. For each requester, emit an alert the first time any sliding 600-second window contains reads of more than D distinct enterprise_person_ids. Break-glass reads count toward the window and are also reported separately. Return (requester_id, window_start_ts, distinct_count). Target O(1) amortised per event and memory proportional to the events resident in the window, not to the day.

Approach

  1. Per requester, hold a deque of (event_ts, person_id) and a hash map from person_id to its occurrence count inside the window, plus a running distinct counter. Push on the right; while the front is older than event_ts minus 600 seconds, pop it, decrement its count and erase the key when the count reaches zero, decrementing the distinct counter. Each event is pushed once and popped once, so the amortised cost is O(1) and the memory is O(W) for window occupancy W.
  2. Test the threshold immediately after each push and nowhere else. Between two consecutive events the window can only lose members as its left edge advances, so the maximum distinct count over all window positions is attained at a position whose right edge is an event. Checking at pushes is therefore exhaustive rather than a sampling approximation.
  3. Latch the alert per requester and re-arm only when the distinct count falls back below D, otherwise one busy stretch emits thousands of near-identical rows and the real signal is buried by its own volume.
  4. Bound memory both per requester and globally. A requester whose window legitimately holds tens of thousands of reads must not hold the process hostage, so cap the deque and degrade above the cap to an approximate distinct counter such as HyperLogLog, stating the error you accept in exchange.
  5. Count break-glass toward the window but carry it in its own output field. Break-glass has to succeed during an emergency, which is exactly why it must be the most visible path in the audit, and excluding it from the count would make the abuse route the quiet one.
  6. State the precondition: this is correct only while input is non-decreasing in event_ts. Out-of-order arrival needs a bounded-lateness buffer and a watermark, and dropping late events without a counter is the failure that hides itself.

Worked solution (25 min)

  1. Implement push, then the eviction loop, then the distinct counter update, in that order, and assert the counter against the map size after each event.
  2. Set D to 25 and feed 15 distinct persons between 09:06:00 and 09:08:59.
  3. Feed 15 more distinct persons from 09:11:00, one every eight seconds.
  4. Record the event at which the alert fires and the window_start it reports.
  5. Re-run the same events through fixed ten-minute tumbling buckets and compare.

Expected result: The 26th distinct read lands at 09:12:20, its window covers 09:02:20 to 09:12:20 and holds all 15 from the first group plus 11 from the second, so the alert is (requester, 09:02:20, 26). Tumbling buckets of 09:00 to 09:10 and 09:10 to 09:20 see 15 and 15, and never fire.

Verify it yourself

  • The distinct counter equals the hash map's size after every event. A missed key erase makes it drift upward and manufacture alerts out of nothing.
  • Shifting every timestamp by a constant produces the same alert with a correspondingly shifted window_start.
  • Processing the stream in two halves sequentially yields the same alerts as one pass, provided the deque survives the split.
  • With D above the day's total distinct persons for that requester, nothing fires.

Follow-up

  • One region's events arrive up to 90 seconds late. What buffer do you add, and what does that do to alert latency?
  • One person uses two requester accounts. What has to change in the key, and what new false positive does that introduce?
  • How do you restore window state after a process restart without replaying the whole day?

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?

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?

Diagnose why an eligibility lookup ignores its index

Medium · indexing · query planning · partial indexes

A 40-million-row coverage_span serves eligibility lookups with: SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = $1 AND valid_to = 'infinity' AND effective_date <= $2 AND (termination_date IS NULL OR termination_date >= $2). An index exists on (effective_date). EXPLAIN (ANALYZE, BUFFERS) shows a sequential scan reading 1.8 million buffers at 2.4 seconds. Explain why that index cannot help, propose the index plus any schema change that makes the predicate index-friendly, and state what you expect the new plan and buffer count to be.

Approach

  1. Read the predicate for selectivity before touching the index. effective_date <= $2 matches most of a 40-million-row history, so a leading range scan on that column returns a large fraction of the table and the planner correctly prefers a sequential scan over millions of random heap fetches. The index is not being ignored; it is being rejected on cost.
  2. Put the equality column first. enterprise_person_id = $1 selects a handful of rows out of 40 million, so it must lead the composite index; a b-tree can only use columns after the first range predicate as filters, not as search bounds.
  3. Turn the open-ended termination into something indexable. The OR ... IS NULL branch forces either a BitmapOr or a post-index filter. Add coverage_end date GENERATED ALWAYS AS (COALESCE(termination_date, DATE '9999-12-31')) STORED and rewrite the predicate to coverage_end >= $2; both COALESCE over a column and a date literal are immutable, so the generated column is legal.
  4. Make the index partial on WHERE valid_to = 'infinity'. Most rows in a bitemporal table are superseded beliefs, so the partial index is a fraction of the full one, stays in cache, and encodes the predicate for free rather than re-checking it per row.
  5. Consider the range alternative honestly: a daterange(effective_date, termination_date + 1, '[)') column with a GiST index and the && operator handles overlap queries and supports an EXCLUDE constraint against overlapping active coverage, but GiST lookups are slower than b-tree for this point-in-time pattern. Pick b-tree for the read path and keep GiST only if you also need the overlap constraint.
  6. Verify rather than assert: re-run EXPLAIN (ANALYZE, BUFFERS) and read Rows Removed by Filter and the shared hit/read split, not just the total time.

Worked solution (30 min)

  1. Run EXPLAIN (ANALYZE, BUFFERS) on the original and write down estimated versus actual rows for each predicate, which shows effective_date <= $2 is not selective.
  2. Add the generated column and rewrite the predicate to use coverage_end.
  3. Create the partial composite index and ANALYZE the table so the planner has statistics for the new column.
  4. Re-run EXPLAIN (ANALYZE, BUFFERS) and confirm an index scan with Rows Removed by Filter at or near zero.
  5. Measure the partial index size against the full-table equivalent with pg_relation_size to confirm the cache argument is real and not assumed.

Expected result: The (effective_date) index is rejected because effective_date <= $2 is a low-selectivity leading range predicate over 40M rows; the planner's estimated cost of random heap access exceeds a sequential scan. Fix: ALTER TABLE coverage_span ADD COLUMN coverage_end date GENERATED ALWAYS AS (COALESCE(termination_date, DATE '9999-12-31')) STORED; CREATE INDEX CONCURRENTLY idx_coverage_active ON coverage_span (enterprise_person_id, effective_date, coverage_end) WHERE valid_to = 'infinity'; and rewrite the last predicate as coverage_end >= $2. The new plan is an Index Scan on idx_coverage_active, Rows Removed by Filter near zero, buffers in the low tens rather than 1.8 million, and sub-millisecond execution.

Verify it yourself

  • Buffers drop from roughly 1.8 million to double digits — an order-of-magnitude change in buffers, not just in wall-clock time, is what proves the index is doing the work.
  • Rows Removed by Filter is 0 or near it; a large value means a predicate is still being re-checked per row and belongs in the index or the partial clause.
  • The rewritten and original predicates return identical rows for a member with an open-ended coverage and for one terminated exactly on $2.
  • pg_relation_size on the partial index is a small fraction of the same index built without the WHERE clause.

Follow-up

  • After the change the plan is still a sequential scan for one particular member. What single query tells you whether that is stale statistics, a genuinely huge row count for that person, or parameter-dependent plan caching?
  • Your generated column needs backfilling on a live table. Does adding a STORED generated column rewrite it, and what does that mean for your maintenance window?
  • How would you serve the same lookup when the answer must be as of a past system-time instant rather than the current belief, given your index is partial on valid_to = 'infinity'?

What is your process for debugging or resolving critical system issues?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  2. Say what you would check first and why it is the highest-information step.
  3. 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 walk us through your previous experience in IT and software development?

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Clarify what is being asked and what a complete answer contains.
  3. Say what you would check first and why it is the highest-information step.

Follow-up

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

How have you approached complex technical challenges in your previous roles?

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. Clarify what is being asked and what a complete answer contains.
  3. Say what you would check first and why it is the highest-information step.

Follow-up

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

Enrolment loading, bitemporal writes and retroactive fan-out

Hard · bitemporal · backpressure · invalidation · retroactive-correction

A weekly X12 enrolment file carries between tens of thousands and two million member rows and routinely terminates coverage retroactively — received on the fifth, effective the first. It writes coverage_span, whose effective_date and termination_date are business time and whose valid_from and valid_to are system time, alongside status and source_file_id. Two files covering overlapping populations can be in flight at once. Design the loader's backpressure, the bitemporal write, and the invalidation fan-out to the eligibility cache and to claims already adjudicated against the now-terminated coverage.

Approach

  1. Never update a coverage_span row. A retroactive termination closes the current row by setting valid_to to now and inserts a new row with the same effective_date, the new termination_date, and valid_from set to now. Both questions then stay answerable: current truth for a past service date filters valid_to = 'infinity' and ranges on business time, while what the system believed on a past date filters valid_from <= that instant < valid_to.
  2. Index for the two axes separately rather than hoping one structure serves both. A btree on (enterprise_person_id, effective_date DESC) partial on valid_to = 'infinity' serves the hot current-truth path; the as-of-system-time path is rare and analytical and can afford a wider scan. A single range index covering both usually makes the common query worse to make the rare one tidier.
  3. Bound the loader instead of running it flat out: chunk into batches of a few thousand rows with a committed checkpoint offset per file, and pace on a downstream signal — replication lag or invalidation queue depth — rather than a fixed sleep. Two million rows written as fast as the database accepts them is how a weekly file becomes a replica-lag incident at 03:00, and the checkpoint is what makes a mid-file crash resumable instead of a full re-run.
  4. Serialise per person rather than per file. Two overlapping files must not interleave versions for one member, so key the work queue on enterprise_person_id and let files proceed concurrently across different members; order within a person's partition by the file's assertion time, and keep source_file_id so a bad file is attributable and reversible as a set.
  5. Fan the consequences out as work, not as a cache flush. Publish the changed enterprise_person_ids so the eligibility cache drops exactly those keys, and enqueue the claim identifiers whose service dates now fall outside coverage. Those claims need reversal and re-adjudication, which touches accumulators, so this is a throttled backlog with its own ordering rather than a synchronous cascade at load time.
  6. Measure the blast radius before it lands: a dry-run pass counting rows whose termination precedes the file's receipt date, and the claims each would affect, turns 'we loaded the file' into a reviewed number. A file retroactively terminating forty thousand members is either a real event or a sender bug, and you want to know which before the reversals run.

Worked solution (45 min)

  1. Write the close-and-insert pair for a retroactive termination, then write the two queries — current truth for a past service date, and belief as of a past instant — and run both against the result.
  2. Write the chunked loader loop with its checkpoint and the downstream signal it paces on, and state the batch size and what it is derived from.
  3. Define the per-person work queue key and show what happens when two overlapping files touch the same member.
  4. Write the fan-out payload and the two consumers, and state the throttle on the claims reprocessing backlog.
  5. Write the dry-run query that counts retroactively terminated members and the claims they affect.

Expected result: Retroactive changes written as close-and-insert across system time with no in-place update, separate indexes for the current-truth and as-of paths, a chunked and checkpointed loader paced on a downstream signal, per-person serialisation that lets overlapping files proceed concurrently, and a published-identifier fan-out feeding cache invalidation and a throttled claims reprocessing backlog.

Verify it yourself

  • After loading the correction, a query as of the day before it arrived still returns the original coverage.
  • Killing the loader mid-file and restarting resumes from the checkpoint without duplicating or skipping rows.
  • Two files touching one member produce a single linear version chain with no interleaving.
  • The eligibility cache stops returning covered for the affected service dates within one invalidation cycle, and the affected claims appear in the reprocessing queue.

Follow-up

  • The sender resends last week's file unchanged. What does the loader write, and how do you prove nothing changed?
  • One file terminates a coverage that a later file reinstates with the same effective date. What does the current-truth query return while both are mid-load?

Gateway instances OOM every five days with flat traffic

Medium · memory leak · heap profiling · idempotency

Interoperability gateway instances are OOM-killed with exit 137 every five to six days. Resident memory grows about 400MB a day per instance and never falls; restarting resets it. Message throughput, connection count and partner mix are unchanged over that period. The gateway keeps an in-process set of recently seen idempotency keys — (sending facility, sending application, message control ID, event timestamp) — in front of the durable ledger. Give the ordered investigation and say what evidence would rule the idempotency cache out.

Approach

  1. Establish that this is a retained-object leak before profiling. Compare resident memory with live heap measured immediately after a forced collection, sampled daily. Resident memory rising while post-collection live heap stays flat points at allocator fragmentation, off-heap or native buffers, or thread stacks, and a heap dump will implicate the wrong object. Rising post-collection live heap is a genuine leak.
  2. Find what the growth is linear in. Plot daily growth against messages processed, connections accepted and wall-clock hours across instances carrying different traffic. Proportional to messages means something is retained per message; proportional to connections means per-connection state is not released on abnormal close, such as a half-open socket whose FIN never arrives.
  3. Diff two heap dumps taken six hours apart on the same instance, sorted by retained size, and identify the dominator. An unbounded set or map shows up as one root holding millions of small entries. This is a five-minute answer once you have the dumps, which is precisely why the first two steps exist: they tell you whether the dump can answer the question at all.
  4. Check the idempotency cache directly against arithmetic rather than suspicion. At single-digit millions of messages a day and a key of roughly 80 bytes plus per-entry overhead, an unevicted set grows by hundreds of megabytes a day, which matches the observed 400MB. A live gauge of its entry count that stays flat is the evidence that rules it out; a gauge tracking messages processed convicts it.
  5. Fix and restore the invariant: bound the cache by size or by a time window, and keep the durable unique constraint as the authority. An in-process set is a latency optimisation and cannot be the correctness mechanism, because it does not survive a restart and is not shared across instances — so a replay right after a deploy would bypass it entirely.
  6. Verify over 48 hours that post-collection live heap is flat, and alert on the cache's entry-count gauge rather than on resident memory, which only tells you after the growth has already happened.

Follow-up

  • How long must the time window be? Tie the number to the sender's replay behaviour after an interface restart and to how far back the broker redelivers.
  • If growth had tracked connections instead of messages, what would you look at first, and how would a half-open MLLP connection show up in the socket table?

Suggested practice checklist

For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.

Day 1 — Rebuild the primitives by implementing them

  • Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
  • Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
  • For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.

Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.

Day 2 — Arrays under an invariant: two pointers, sliding window, binary search

  • Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
  • Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
  • Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.

Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.

Day 3 — Sorting, heaps, and the greedy argument that has to be proved

  • Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
  • Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
  • Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.

Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.

Day 4 — Recursion, memoisation, and the step to a table

  • Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
  • Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
  • Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.

Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.

Day 5 — Graphs, where most of the work is choosing the traversal

  • Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
  • Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
  • Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.

Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.

Day 6 — One day for everything that is not an algorithm

  • Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
  • Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
  • Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.

Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.

Day 7 — Solve out loud, under time

  • Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
  • Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
  • Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.

Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.

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.

How do you handle technical debt while meeting project deadlines?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

  • What did you decide not to do, and why?
  • How did you know your change caused the improvement?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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?

Take a review disagreement about upserting corrected lab results

Medium · code review · versioning · unique constraints · idempotency

A colleague's result consumer writes observation_result with INSERT ... ON CONFLICT (filler_order_id) DO UPDATE SET value_numeric, result_status, issued_ts. Corrections arrive carrying the same filler order identifier as the original, and their fixture suite — one final result per order — passes. They argue the table stays bounded and reads need no version predicate, and you are the only objector. Reconstruct the review: the comment you wrote, how you demonstrated the defect without a full reproduction, the uniqueness key you proposed instead, and what you would have accepted.

Approach

  1. The probe is whether you can make a correctness objection land on someone who has passing tests. Name what the upsert destroys in specifics — the prior value_numeric, its issued_ts and its result_status — so that no query can answer what was displayed before the correction arrived, which is the exact question an incident review or a legal hold asks.
  2. Demonstrate inside their own suite rather than arguing. Add one message: same filler order and LOINC code, result_status 'corrected', a different value. Point at the row count staying constant and the original value becoming unselectable. A failing case in their fixture is more persuasive than a paragraph.
  3. Lead with the sharper defect, which is the key itself. One accession fans out to many analytes, so filler_order_id is not unique even across originals; the unique index their ON CONFLICT requires cannot exist without collapsing a panel to a single row. Propose UNIQUE (filler_order_id, loinc_code, version), with the insert carrying supersedes_observation_id.
  4. Deal with their read-simplicity objection honestly instead of dismissing it. With a backwards supersedes pointer, 'current' means 'no row supersedes this one', which is an anti-join; a real design therefore adds a current-version marker maintained in the same transaction as the insert, or a per-(order, analyte) pointer table. Versioning is not free and saying so is what makes the rest credible.
  5. Name the second consequence, for consumers rather than for storage: an in-place update emits no state transition, so anything that already acted on the preliminary value never learns it changed. Versioned inserts produce that correction event as a by-product.
  6. Close on conduct: what you wrote, whether you blocked the merge, who decided, and the smaller thing you would have accepted — shipping the versioned insert without the current-version marker and eating the anti-join until it shows up in p99.

Follow-up

  • The same corrected message is replayed a week later during an interface restart. Walk both designs through it.
  • How do you keep the current-version marker correct when two corrections for the same analyte are processed concurrently?
  • A result arrives with status 'entered_in_error'. Is that a new version, a deletion, or something else, and what does a chart show afterwards?