Interview guides · AMN Healthcare
AMN Healthcare Software Engineer Interview Questions & Guide 2026
System design practice for AMN Healthcare software engineer candidates: reach a data model and an API contract, not a box diagram.
The 60-second brief
As a Software Engineer at AMN Healthcare, you are at the heart of the digital transformation within the healthcare staffing and workforce solutions industry. Your work directly impacts the efficiency of healthcare delivery by building and maintaining the platforms that connect qualified medical professionals with the facilities that need them most. This role is critical, as it requires balancing high-speed technical output with the reliability and security necessary in a healthcare-adjacent environment.
Allocate preparation against your weakest link rather than your favourite topic. A strong algorithm habit usually comes with weak out-loud explanation of tradeoffs, and years of shipping usually come with rusty from-scratch implementation under a clock.
Focus areas
- Version clinical results instead of updating rows in place
- Model bitemporal coverage and retroactive eligibility changes
- Make HL7 and X12 ingestion idempotent under replay
Overview
You will contribute to complex, mission-critical systems that operate at a significant scale. Whether you are focusing on backend infrastructure, internal management tools, or candidate-facing applications, your code helps bridge the gap between staffing logistics and patient care. The environment is dynamic and collaborative, demanding a combination of strong technical proficiency and the ability to work effectively within a cross-functional team of product managers, designers, and operational stakeholders.
While the interview process can vary in intensity, successful candidates often highlight that the team culture is welcoming and professional, focusing on both your technical depth and your ability to thrive in a team-oriented environment.
The AMN Healthcare interview process
AMN Healthcare candidates report 5 rounds · ≈ 4-6 weeks. The stages below are what candidates describe, not a published process.
Initial Screening
Before anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.
What to demonstrate
- Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
- Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
- Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled
How to prepare
- Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
- Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
- Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market
Technical Discussions
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
Behavioral Discussions
Your first answer is not really what is scored. It buys the follow-up questions, and those decide the round. An interviewer with fifteen minutes takes one thread and pushes on it four or five times, so a story you can only tell at a single level of detail collapses under the third why. That is an argument for fewer stories known deeply rather than one prepared per prompt. Four or five pieces of work you can still explain down to the code you changed and the argument you had about it will cover nearly anything asked in this round.
What to demonstrate
- Whether a story holds as the questioning moves from what you did to why that instead of the alternative, and then to what you would change knowing what you know now
- Whether you can re-cut a project to answer the question actually asked rather than delivering a rehearsed block that answers an adjacent one
- Whether your level of detail is chosen rather than habitual: going down to the schema when the question is about the data model, staying out of it when the question is about the person who disagreed with you
How to prepare
- Pick four projects and write the chain out four levels deep for each: what you did, why that, why not the alternative, and what would have to be true for the alternative to have won. Where you cannot reach the fourth level, you have a placeholder rather than a story
- Have someone ask why three times in a row on a single thread with nothing else added, and mark the point where you start repeating a sentence you already said. That point is where the interviewer stops learning anything
- Build a one-page index instead of an answer bank: the common prompts in this round (disagreement, a failure that was yours, thin requirements, a deadline you missed, work you inherited) mapped to which of your four projects you would use for each, so the choosing is done now rather than while an interviewer waits
In-depth Interviews
Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
Final Management Interviews
Where the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.
What to demonstrate
- Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
- Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
- Whether you establish what decision is on the table before proposing anything
- Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip
How to prepare
- Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
- Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
- Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it
Where candidates lose points
Assuming admission, discharge and transfer messages arrive in the order the events happened.
Interface engines route by message type across separate queues and retry independently, so a discharge can land before the admission it closes and an update can land before the registration it modifies. Ordering has to come from the sender's event timestamp plus a per-encounter sequence, and the consumer has to apply out-of-order and late-arriving events correctly rather than rejecting them, because rejection turns a recoverable ordering issue into permanent data loss that nobody notices until a report is short.
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.
Trusting input because it came from your own front end
Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.
A cache with no invalidation story
Say how an entry goes stale, how long you can serve it stale, and what happens when many requests miss the same key at the same instant. One popular key expiring under load sends every concurrent request to the origin together; single-flight coalescing, jittered expiry, or serving stale while revalidating are the standard answers.
Questions & practice
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.
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?
Flatten overlapping coverage spans into a primary-payer timeline
Medium · sweep line · intervals · bitemporal
For one enterprise_person_id you hold up to 10,000 coverage_span rows: coverage_id, payer_id, plan_id, effective_date, termination_date (null means open-ended), coverage_order (1 is primary), status, valid_from and valid_to (system time). Given a system instant T and a date range D0 to D1, return disjoint business-date intervals covering that range, each labelled with the active coverage_id of lowest coverage_order, and with uncovered stretches returned explicitly. termination_date is the last covered day. Only status active counts. Target O(N log N) time, O(N) space.
Approach
- Apply the system-time slice before any interval logic: keep rows where valid_from is at or before T and valid_to is after T. That single predicate is what makes the answer 'what we believed at T' rather than 'what we believe now', and a retroactive termination loaded after T leaks straight into the output if you skip it.
- Convert each surviving row to a half-open day interval from effective_date up to termination_date plus one day, using positive infinity for a null termination. Half-open removes every off-by-one at the join between a termination and the next plan's effective date, which is where same-day switches get turned into false gaps.
- Sweep: emit 2N boundary events, sort by date in O(N log N), and hold the active set in a min-heap keyed by (coverage_order, coverage_id) with lazy deletion, popping the top while it has already expired at the current boundary. Each coverage is pushed once and popped once, so the sweep is O(N log N) total and O(N) space.
- Emit an output interval whenever the heap top changes between consecutive boundaries, and emit an explicit uncovered interval when the heap empties. A gap reported as a gap is a different answer from a gap omitted, and downstream the difference is a denial versus a silent assumption of coverage.
- Fix the tie-break and state it: lowest coverage_order, then lowest coverage_id. Two rows at order 1 is a data error, and without a deterministic rule the coordination-of-benefits answer changes between two runs over identical data.
- Clip to D0 and D1 last, so a coverage that starts before D0 still contributes its correct order inside the window.
Worked solution (30 min)
- Filter to rows live at T, then map each to a half-open interval and a (coverage_order, coverage_id) priority.
- Build the boundary list, sort it, and sweep with a lazily-deleted min-heap.
- At each boundary, drop expired heap tops and compare the new top to the previous one, emitting an interval on change.
- Emit an uncovered interval whenever the heap is empty between two boundaries.
- Clip the emitted list to D0 through D1 and assert the intervals are disjoint and contiguous.
Expected result: Window 2026-01-01 to 2026-12-31 with three live rows: coverage 1 at order 1 effective 01-01 terminating 06-30, coverage 2 at order 2 effective 03-01 open-ended, coverage 3 at order 1 effective 09-01 open-ended. Output is three intervals: 01-01 to 06-30 labelled coverage 1, 07-01 to 08-31 labelled coverage 2, 09-01 to 12-31 labelled coverage 3, with no gaps. Delete coverage 2 and the middle interval becomes an explicit uncovered stretch of 07-01 to 08-31.
Verify it yourself
- The emitted intervals are sorted, pairwise disjoint, and their union is exactly D0 through D1 once uncovered stretches are included.
- Every boundary date in the output is D0, the day after D1, some effective_date, or some termination_date plus one day. Any other date means a boundary was synthesised.
- Removing the lowest-order coverage promotes exactly the next order and leaves every boundary it did not touch unchanged.
- Choosing T before the load time of a retroactive termination returns the pre-correction timeline, and T after it returns the corrected one, from the same table.
Follow-up
- Run the same query at two system instants and diff the timelines. What did the correction change, and what does each extra instant cost you?
- A termination arrives with an effective date earlier than the service date of a claim that already adjudicated and paid. What does the timeline now say, and what has to happen to that claim?
- Two enrolment files disagree on coverage_order for a dependent. What does your tie-break do, and what should the system do instead of tie-breaking?
Reconstruct what the chart displayed at five million past instants
Hard · external sort · as-of join · io bound
observation_result holds 200 million versions: observation_id, enterprise_person_id, filler_order_id, loinc_code, value_numeric, result_status, collected_ts, issued_ts, version, supersedes_observation_id. An incident review hands you 5 million queries of (enterprise_person_id, filler_order_id, loinc_code, as_of_instant). For each, return the version that was visible at that instant, meaning the one with the greatest issued_ts at or before it. Neither side fits in memory. The per-query scan is correct. Explain precisely why it is too slow, then give a plan with its complexity.
Approach
- Name the clock before naming an algorithm. Visibility is issued_ts, the release time. collected_ts is when the specimen was drawn and can precede release by hours, so ordering on it reports a correction as visible long before anyone could have seen it. No amount of index work rescues the wrong column.
- Be exact about why the naive plan fails, because the interviewer is testing whether you can tell arithmetic cost from I/O cost. Per query the chain scan is O(V_k) and the arithmetic is trivial, but 5 million independent lookups into a 200-million-row structure that does not fit in RAM is 5 million random reads. The job is bounded by seeks per query, not by comparisons, and buying a faster comparison changes nothing.
- Convert random access into sequential access. Hash-partition both sides on the chain key (enterprise_person_id, filler_order_id, loinc_code) into P shards sized to fit memory, sort each shard's versions by (chain key, issued_ts) and its queries by (chain key, as_of_instant), and sweep the pair in lockstep. Total O((V + Q) log(V + Q)) with external sort, replacing Q seeks with two sequential passes.
- Inside a chain the sweep is linear, not logarithmic, because queries are visited in ascending as_of order and the version pointer only moves forward: O(V_k + Q_k) per chain. Binary search per query is the better shape only when Q is small relative to V and the versions are already indexed and resident.
- Return the empty answer as a distinct outcome. A query whose as_of precedes the first issued_ts means nothing was displayed, which is not the same as the earliest value, and is frequently the exact fact the review is chasing.
- Return a version later marked entered_in_error if it was live at the instant asked about. Reconstructing the past means reporting what was on the screen, including what was wrong, and quietly substituting today's truth defeats the purpose of the exercise.
Follow-up
- Read replicas lagged 40 seconds at the time. Does your answer describe what the clinician actually saw, and how would you bound the difference?
- Serve the same question online for a single chart at a p99 under 50ms. What changes?
- One partner changed its filler_order_id format mid-year, so the chain key is not stable. How does that appear in your output, and how do you detect it rather than returning empty answers?
Bitemporal coverage table with retroactive termination queries
Easy · bitemporal · schema design · retroactive correction
You maintain coverage_span: coverage_id, enterprise_person_id, payer_id, plan_id, effective_date, termination_date (nullable, meaning open-ended, not unknown), status, valid_from timestamptz, valid_to timestamptz DEFAULT 'infinity'. An enrolment file received on 2026-03-05 terminates a member effective 2026-03-01, after a claim for service date 2026-03-03 was already adjudicated against the answer then in force. State your termination_date convention, write the two SELECTs — (a) is the person covered on 2026-03-03 as known now, (b) what the system believed on 2026-03-04 — and say precisely what the loader writes when the file lands.
Approach
- Separate the two time axes explicitly before writing anything: effective_date/termination_date are business time (when benefit applied), valid_from/valid_to are system time (when this row was the system's belief). Every query names both, or it is ambiguous.
- Pin the inclusivity convention, because the predicate depends on it. Enrolment files conventionally carry termination_date as the last covered day, so the business-time predicate is effective_date <= :svc AND (termination_date IS NULL OR termination_date >= :svc). If your feed uses an exclusive end, it becomes > :svc, and a half-open daterange column removes the ambiguity permanently.
- Write the as-known-now query by pinning system time to the open version (valid_to = 'infinity') and letting business time float to the service date.
- Write the as-of-belief query by pinning system time to an instant: valid_from <= :asof AND valid_to > :asof. Half-open on both ends is what makes 'exactly one row per key per instant' provable.
- Describe the write as one guarded statement rather than two independent ones: a data-modifying CTE whose UPDATE stamps the open row's valid_to to now() (the only UPDATE the design permits — it stamps system time, it never edits business facts) and returns that row's keys, with the INSERT of the new version selecting FROM that CTE. Feeding the INSERT from the UPDATE's RETURNING is what makes a replayed file a no-op; an unconditional INSERT leaves the partial unique index to raise 23505 instead, which is a backstop rather than idempotency. The pre-correction row stays readable either way, which is what makes the already-adjudicated claim defensible.
Worked solution (25 min)
- Write the DDL with both ranges half-open in system time: valid_from timestamptz NOT NULL, valid_to timestamptz NOT NULL DEFAULT 'infinity', and a partial unique index on (enterprise_person_id, plan_id, effective_date) WHERE valid_to = 'infinity' so only one belief is open per business period.
- Write query (a) against the open system-time version, with the business-time predicate on the service date.
- Write query (b) by replacing valid_to = 'infinity' with the half-open instant predicate, keeping the identical business-time predicate so the only thing that changed is which belief you read.
- Write the loader as one statement: WITH closed AS (UPDATE ... RETURNING ...) feeding INSERT ... SELECT FROM closed. Guard the UPDATE with valid_to = 'infinity' so a concurrent loader cannot close the same row twice, and with a termination_date IS DISTINCT FROM :new_term test so a replay of the same file matches no row and therefore inserts none. Under READ COMMITTED a second loader that blocks on the row lock re-evaluates the guard against the newly committed version, fails it, and writes nothing.
- Trace the 2026-03-05 file by hand: before it, one row (effective 2026-01-01, termination NULL, valid_to infinity); after it, two rows, and confirm (a) returns not-covered while (b) returns covered.
Expected result: (a) SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = :p AND valid_to = 'infinity' AND status = 'active' AND effective_date <= DATE '2026-03-03' AND (termination_date IS NULL OR termination_date >= DATE '2026-03-03'); returns no row. (b) the same query with valid_to = 'infinity' replaced by valid_from <= TIMESTAMPTZ '2026-03-04 00:00+00' AND valid_to > TIMESTAMPTZ '2026-03-04 00:00+00'; returns the pre-correction row with termination_date NULL. The loader runs as one statement: WITH closed AS ( UPDATE coverage_span SET valid_to = now() WHERE enterprise_person_id = :p AND plan_id = :pl AND effective_date = DATE '2026-01-01' AND valid_to = 'infinity' AND (termination_date IS DISTINCT FROM DATE '2026-03-01' OR status IS DISTINCT FROM 'terminated') RETURNING enterprise_person_id, payer_id, plan_id, effective_date ) INSERT INTO coverage_span (enterprise_person_id, payer_id, plan_id, effective_date, termination_date, status, valid_from, valid_to, source_file_id) SELECT enterprise_person_id, payer_id, plan_id, effective_date, DATE '2026-03-01', 'terminated', now(), 'infinity', :f FROM closed;
Verify it yourself
- For any fixed instant, the system-time predicate selects exactly one row per (person, plan, business period): SELECT enterprise_person_id, plan_id, effective_date, count() FROM coverage_span WHERE valid_from <= :t AND valid_to > :t GROUP BY 1,2,3 HAVING count() > 1 returns zero rows.
- Query (b) with :asof = now() returns exactly what query (a) returns — if it does not, the close-and-insert is not atomic or the half-open boundaries overlap.
- No row's valid_from equals its valid_to, and no two versions of one coverage_id have overlapping [valid_from, valid_to).
- Replaying the same enrolment file inserts nothing and raises nothing: the guarded UPDATE matches no row because the open version already carries termination_date 2026-03-01 and status 'terminated', and the INSERT selects FROM that UPDATE's RETURNING, so it has zero input rows. Assert the person's row count is unchanged. A loader that instead runs the INSERT unconditionally fails the replay with SQLSTATE 23505 from the partial unique index — that stops the duplicate, but it is an error the caller must handle, not an idempotent replay.
Follow-up
- The claim adjudicated on 2026-03-03 is now retroactively uncovered. What does reprocessing have to read — the belief at adjudication time or the current truth — and what does the remittance reversal reference?
- Add the constraint that stops two active coverage versions from overlapping in business time for the same person and plan, without blocking the superseded history rows.
- A later file reinstates coverage with the same effective_date. How many rows exist for that period afterwards, and which one does query (a) return?
Namespace source identifiers so a cross-facility join cannot collide
Easy · identity · composite keys · constraints
person_identity_link holds one row per (assigning_authority, source_person_id) per link version: link_id, enterprise_person_id, assigning_authority, source_person_id, match_score, link_status in ('auto_linked','manual_linked','potential_duplicate','unlinked','rejected'), version, superseded_by_link_id, decided_by, decided_at. Two facilities in one network both issued medical record number 004821, to different people. An existing extract joins on source_person_id alone and has been merging their charts. Give the constraint set that makes the bare join impossible to write by accident, then write the query that resolves one facility's (authority, MRN) to its current enterprise_person_id.
Approach
- Name the root cause precisely: an MRN is unique only inside the authority that issued it, and a member ID only inside payer plus plan. The identifier is half a key; the namespace is the other half.
- Make the composite the only addressable key. UNIQUE (assigning_authority, source_person_id, version) gives version history; a partial unique index on (assigning_authority, source_person_id) WHERE superseded_by_link_id IS NULL enforces one live decision per source record.
- Remove the bare column as a join target from every consuming view: expose a view or function that takes both arguments, so a one-argument lookup is a compile-time error rather than a silent cross-patient merge.
- Write the resolution query against the live version only, and filter link_status separately — 'unlinked' and 'rejected' are live rows that mean 'no enterprise identity', so they must not be confused with 'no row'.
- State the detection query you would run before trusting any existing extract: group source_person_id across authorities and count distinct authorities, which surfaces every collision already in the data.
Follow-up
- The spine stores enterprise_person_id directly on encounter and claim_line. What does a wrong merge cost you with that design, and what would you store instead to keep unmerge a supported operation?
- A link is downgraded from auto_linked to potential_duplicate. Should the resolution query return the old enterprise identity, nothing, or an error, and who decides?
- Your partial unique index blocks a second live row. How does the merge path insert the new version and supersede the old one without violating it mid-transaction?
How long have you been coding?
Medium
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- 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?
How do you ensure your code meets quality and performance standards?
Medium
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Identity resolution at registration peak with reversible merges
Hard · blocking · probabilistic-matching · availability · merge-reversibility
Identity resolution sits in front of registration: one to three thousand match calls per second at morning peak against tens of millions of person records, a 200ms budget, and an availability requirement higher than the systems calling it. It writes person_identity_link (enterprise_person_id, assigning_authority, source_person_id, match_score, link_status, version, superseded_by_link_id, decided_by, decided_at). Design candidate generation, the deterministic-then-probabilistic pipeline and its outcome bands, and what registration does when the service is degraded — all without making a merge irreversible.
Approach
- Kill the naive framing with arithmetic: scoring an inbound record against tens of millions of candidates is on the order of 10^7 comparisons per call at up to 3,000 calls per second, which no scoring function survives. Block first — several cheap candidate-generation passes such as phonetic surname with birth year, normalised phone, and normalised address with birth date — unioned, cutting the candidate set to tens. Recall is now bounded by the blocking keys, which is why several passes beat one clever one.
- Run deterministic rules first and short-circuit. An exact hit on (assigning_authority, source_person_id) is a btree lookup producing a link with no score at all. Probabilistic scoring runs only when deterministic fails, which keeps the median call in single-digit milliseconds and leaves the 200ms budget for the tail.
- Define three outcomes, not two: auto-link above an upper threshold, reject below a lower one, and potential_duplicate in between routed to human review. One threshold forces a choice between merging two different people and fragmenting one person's record, and the middle band is exactly where both errors live.
- Implement merge as a new link version rather than a rewrite. The losing identity's source records get new person_identity_link rows pointing at the winner, with superseded_by_link_id set on the prior rows; unmerge restores the previous versions. Rewriting downstream foreign keys in encounter and claim_line destroys the original attribution and makes unmerge guesswork. You accept an extra resolution step on every patient-scoped query in exchange.
- Degrade deliberately and asymmetrically. Registration cannot block on a matcher outage, so fall back to a read replica of the deterministic index, and when that also fails, issue a provisional enterprise_person_id marked unresolved with the encounter queued for resolution on recovery. Degrade toward fragmentation, which is reversible, never toward merging, which is only reversible because step four held.
- Close the read-your-writes gap the replica introduces: a link created seconds ago may be absent from the replica, so the same person registering twice in two minutes can acquire two provisional identities. A small write-through cache keyed on (assigning_authority, source_person_id) handles the common case without making the whole path synchronous.
Worked solution (45 min)
- Compute comparisons per call with and without blocking, and state the candidate-set size each blocking pass yields.
- Write the deterministic short-circuit and the probabilistic path, with the two thresholds and what happens in the middle band.
- Write the merge as inserts into person_identity_link, then write the unmerge that reverses it, and trace a claim posted under the losing identity through both.
- Define degraded behaviour at two levels — replica-only and fully unavailable — and state which error each one deliberately prefers.
- Show the read-your-writes case and the cache that resolves it.
Expected result: Multi-pass blocking that makes per-call comparison counts tractable, deterministic matching short-circuiting ahead of probabilistic scoring, three outcome bands with a review queue for the middle, merge and unmerge expressed purely as link versions leaving downstream rows untouched, and degradation that prefers a reversible duplicate over an irreversible merge.
Verify it yourself
- A merge followed by an unmerge restores every source record to its original enterprise identity, including one that had a claim posted under it.
- No downstream table's person foreign key is written by the merge path.
- With the matcher's scoring path disabled, registration still completes and marks the identity unresolved.
- Per-call candidate counts stay in the tens across a sample of inbound records, including common surnames.
Follow-up
- Two different people sharing a name and birth date were auto-linked last month and a lab result crossed charts. Walk through the unmerge and what each downstream consumer is told.
- You want to raise the auto-link threshold. What do you measure first, on what labelled data, and what does the change cost the review queue?
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
Roughly ninety minutes on weeknights with one longer weekend block. The plan cuts scope rather than compressing everything, on the assumption that one thing finished per night beats four half-started.
Day 1 — Fix the scope and take a cold baseline
- Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list and keep it visible all week.
- Take one twenty-five-minute coding problem and one fifteen-minute design prompt cold, and write the single sentence naming what blocked each, because those two sentences decide where the remaining evenings go.
- Set the week's rule: one thing finished every night, including the night you only have forty minutes.
Deliverable: A one-page scope with a not-doing list and two cold attempts, each carrying one sentence on what blocked it.
Day 2 — One pattern, written three times from blank
- Choose the single pattern most likely to appear in your loop and write it three times from an empty file rather than editing the previous attempt.
- On the third pass, write the invariant as a comment before the loop body and the complexity before the first line of code.
- Stop at ninety minutes even if the third version is imperfect, and write the one thing you would fix given another hour.
Deliverable: Three independent implementations of the same pattern plus a note on what changed between them.
Day 3 — One design, only to the depth you can defend
- Take one system shape and go only as far as requirements, interface and data model, refusing to draw a box you could not survive a follow-up about.
- Attach one number to each non-functional requirement, deriving it rather than asserting it, and write the assumption the number rests on.
- Write the one tradeoff you are choosing against and the observation that would make you reverse it.
Deliverable: One design at interface-and-schema depth with derived numbers and one written reversible tradeoff.
Day 4 — Only the fundamentals you will have to defend
- Write, in under two hundred words each, the answers to the two questions that follow almost any implementation: why this structure and not the obvious alternative, and what happens to this code at a hundred times the input.
- Write what an index actually costs: faster lookups on the indexed columns against a write that now maintains a second structure, plus the cases where the planner declines to use it anyway, low selectivity, or a predicate wrapping the column in a function.
- Delete any answer you cannot deliver aloud in under a minute, since an answer that needs reading is not an answer you have.
Deliverable: Three written answers, each under two hundred words and each timed aloud.
Day 5 — Your own work, timed
- Write a ninety-second and a four-minute version of your main project and time both aloud rather than reading them.
- Prepare the two follow-ups that always come: what you would do differently, and how you knew it worked.
- Put one number in the first sentence and be ready to say exactly where it came from and what it excludes.
Deliverable: Two timed narratives with one defensible number in the opening line.
Day 6 — The one full rehearsal, in the weekend block
- Run a sixty-minute mock covering a coding round and a design round in one sitting with no break, because sustained attention is the thing evenings have not trained.
- Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
- Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.
Deliverable: Mock notes naming three failure moments with a specific fix written under each.
Day 7 — Taper
- Write the twenty-minute warm-up you will actually do on the morning: one problem you can already solve from a blank file, one design you can narrate, and nothing you have never seen.
- Re-read only your own notes from this week and open no new material.
- Write the logistics down: the editor or shared document you will be working in, whether execution and lookups are permitted, and the sentence you will use when you do not know something.
Deliverable: A one-page card holding the design structure, the project numbers, and the logistics.
Disagreement needs an outcome you did not choose
Conflict answers where you were right and everyone came round are the weakest ones. Stronger: the evidence you went and collected, what would have changed your mind, and what you did in the weeks after the call went against you. Implementing a design you argued against, properly, is a specific and checkable behaviour.
Can you describe your experience with specific technical items related to your field?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
What management style do you prefer?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
- 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?
How do you manage teamwork scenarios or conflicts within a project?
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.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
Can you tell me about yourself?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?