Interview guides · Christus health
Christus health Software Engineer Interview Questions & Guide 2026
Concurrency drills for Christus health software engineer prep: races, lock ordering, and what a check-then-act sequence assumes.
The 60-second brief
As a Software Engineer at Christus health, you play a vital role in building, maintaining, and scaling the technology infrastructure that supports a large-scale healthcare network. This position sits at the intersection of modern software development and critical healthcare operations, ensuring that clinical and administrative systems run reliably, securely, and efficiently. Your daily work directly impacts the tools used by healthcare professionals, administrative staff, and patients across multiple regions.
Getting the code to run is the floor. What usually separates answers is the case checked without prompting: empty input, a single element, duplicate keys, or a value that overflows the integer type you chose.
Focus areas
- Version clinical results instead of updating rows in place
- Make HL7 and X12 ingestion idempotent under replay
- Design person merges that remain reversible afterwards
Overview
The scope of this role spans enterprise backup operations, application system analysis, data management, and core software development. You will work extensively with enterprise platforms, data pipelines, and specialized health-tech systems such as clinical and revenue cycle applications. Because healthcare environments demand high availability and strict data integrity, your contributions directly safeguard critical hospital operations and patient care workflows.
While some legacy technologies remain part of the enterprise ecosystem, the engineering challenges you face will test your ability to bridge older systems with modern architectures. You will collaborate closely with cross-functional teams, including IT operations, database administrators, and clinical stakeholders, to translate complex business needs into robust technical solutions. Expect an environment that values diligence, mission alignment, and technical precision.
The Christus health interview process
Christus health candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screen
The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.
What to demonstrate
- Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
- Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
- Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural
How to prepare
- Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
- Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
- Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one
Technical Assessments
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Interviews with Engineering Managers
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
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.
Storing monetary amounts as floating-point numbers.
Binary floating point cannot represent values like 0.10 exactly, so summing allowed and paid amounts across millions of claim lines accumulates representation and rounding error. Reconciliation against a remittance file is exact to the cent, so the drift surfaces as a balance that is off by a few cents with no identifiable cause, and engineers lose days looking for a logic bug in code that is correct apart from its numeric type. Integer minor units or an exact decimal type removes the class entirely.
Comparing floating-point values for equality, or holding money in them
Binary floating point cannot represent 0.1 exactly, so repeated addition drifts and an equality check fails on values that are mathematically equal. Store currency as integer minor units or a decimal type, and compare floats against a tolerance you chose for a stated reason.
Not asking what the system looks like if it dies halfway through
For any multi-step write, say what state remains if the process stops between step two and step three, and what brings it back: a single transaction, a saga with compensating actions, an outbox, or a reconciliation job. Partial failure is routine at any real call volume, so 'that shouldn't happen' is an answer with nothing behind it.
Questions & practice
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?
Resolve enterprise identity from a link log that supports unmerge
Hard · union-find · rollback · identity resolution
person_identity_link holds link_id, enterprise_person_id, assigning_authority, source_person_id, match_score, link_status (auto_linked, manual_linked, potential_duplicate, unlinked, rejected), version, superseded_by_link_id, decided_by, decided_at. You have up to 40 million source identities and 60 million decisions applied in decided_at order. Build a structure answering which enterprise identity a given (assigning_authority, source_person_id) resolves to after any prefix of the log, and supporting a revert of the k most recent merges at O(1) each. State the query complexity, and say why path compression is unavailable to you.
Approach
- Intern the node key as the pair (assigning_authority, source_person_id) into a dense integer index. Never key on source_person_id alone: two facilities in one network routinely issue the same medical record number to different people, so a bare-value key merges two patients before any matching logic has run, and a single-source test fixture will never show it.
- Classify the log rows before touching the structure. auto_linked and manual_linked are unions; potential_duplicate and rejected are recorded decisions that must not union anything; unlinked is a revert of the link it supersedes, not a new edge.
- Use union by size with an explicit undo stack, pushing (attached_root, its previous parent, the previous size of the absorbing root) on every union. Find walks parent pointers to the root: union by size bounds tree height at log2(n), so a query is O(log n) worst case, a union is O(log n), and a revert pops two words and is O(1).
- Path compression is the thing you have to give up, and the reason is specific: it rewrites parent pointers of nodes that were never named in the union being recorded, so the undo entry no longer describes the mutation that happened and a revert restores a forest that is quietly wrong. Near-constant find is only available to an append-only structure.
- Hold enterprise_person_id as a property of the root slot rather than a value copied onto every member. A merge then relabels one slot instead of n rows, and an unmerge restores two labels instead of reconstructing n.
- State the price plainly: every read path gains a resolution hop and no downstream table can carry a plain patient_id column. That cost is what buys reversibility, and it belongs in the design discussion rather than being discovered later by whoever writes the first join.
Worked solution (40 min)
- Intern three nodes: A is (AUTH1, MRN-77), B is (AUTH2, MRN-77), C is (AUTH1, MRN-90).
- Apply union(A, C) from a manual_linked row, pushing its undo entry.
- Apply union(A, B) from an auto_linked row with a high match score, pushing its undo entry.
- Query all three, then pop one undo entry and query all three again.
- Assert the parent and size arrays element-wise against a snapshot taken before the second union.
Expected result: After the first union, A and C share a root and B is its own root. After the second, all three share a root. After reverting one union, B is its own root again and A and C remain merged, with the first union's record untouched. Note what the key bought you: A and B collide on MRN-77 and would have been one identity from the first insert under a bare-value key, with no decision row ever recorded.
Verify it yourself
- After reverting all k unions, the parent and size arrays are identical element by element to the pre-union snapshot, not merely equal in their query answers.
- Tree height never exceeds log2(n). Assert it, because one union that attaches by arbitrary root instead of by size breaks the find bound without any visible symptom.
- Applying a potential_duplicate or rejected row changes no query answer and pushes nothing onto the undo stack.
- Querying (AUTH1, MRN-77) and (AUTH2, MRN-77) before any link row returns two distinct identities.
Follow-up
- A merge is found wrong three weeks and 200,000 unions later, and your stack only reverts a suffix. What do you do instead, and what does that cost?
- A claim was posted under the losing identity before the merge. After the unmerge, which identity owns it, and what in your design let you answer that?
- Two matcher instances submit unions concurrently. What is the smallest change that keeps both the structure and the undo stack consistent?
Flatten overlapping coverage spans into a primary-payer timeline
Medium · sweep line · intervals · bitemporal
For one enterprise_person_id you hold up to 10,000 coverage_span rows: coverage_id, payer_id, plan_id, effective_date, termination_date (null means open-ended), coverage_order (1 is primary), status, valid_from and valid_to (system time). Given a system instant T and a date range D0 to D1, return disjoint business-date intervals covering that range, each labelled with the active coverage_id of lowest coverage_order, and with uncovered stretches returned explicitly. termination_date is the last covered day. Only status active counts. Target O(N log N) time, O(N) space.
Approach
- 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.
Follow-up
- Run the same query at two system instants and diff the timelines. What did the correction change, and what does each extra instant cost you?
- A termination arrives with an effective date earlier than the service date of a claim that already adjudicated and paid. What does the timeline now say, and what has to happen to that claim?
- Two enrolment files disagree on coverage_order for a dependent. What does your tie-break do, and what should the system do instead of tie-breaking?
Add a backfilled NOT NULL column to a live encounter table
Hard · online migration · ddl locks · backfill
encounter holds 900 million rows and is written continuously by the admit/discharge/transfer consumer. You must add admission_source_code text, backfill it per row from a lookup on the source message archive, constrain it to a fixed code set, make it NOT NULL, and add an index on (facility_id, admit_ts) — with no write outage and no statement blocked longer than one second. Give the ordered steps, the lock level each takes, why the obvious single ALTER is unavailable here, and the session setting that stops a DDL statement from stalling everything behind it.
Approach
- Say why the shortcut does not apply. Since PostgreSQL 11, ADD COLUMN with a constant default is metadata-only and cheap, but this value is derived per row from an archive lookup, so there is no constant to store and the table must be written row by row regardless.
- Add the column nullable first: catalog-only, ACCESS EXCLUSIVE for microseconds. The danger is not the statement's duration but the lock queue — a pending ACCESS EXCLUSIVE blocks every reader that arrives behind it, so a long-running report turns a microsecond DDL into a multi-minute outage. SET lock_timeout = '1s' before the ALTER and retry on failure; that converts a potential outage into a retried statement.
- Deploy dual-write before backfilling. The consumer must populate the column on every insert and transfer from that moment, or the backfill chases a moving tail forever.
- Backfill in bounded batches over primary-key ranges, a few thousand rows per transaction with a pause between, restricted to WHERE admission_source_code IS NULL so it is resumable and idempotent. One giant UPDATE holds a transaction open for hours, pins the xmin horizon so autovacuum cannot clean anything, and bloats the table by a full row version per updated row.
- Add the value constraint as CHECK (...) NOT VALID first — ACCESS EXCLUSIVE, no scan — then ALTER TABLE ... VALIDATE CONSTRAINT, which takes only SHARE UPDATE EXCLUSIVE and scans without blocking reads or writes.
- Reach NOT NULL without a blocking scan: add CHECK (admission_source_code IS NOT NULL) NOT VALID, VALIDATE it, then SET NOT NULL, which on PostgreSQL 12 and later uses the validated constraint as proof and skips the full scan; drop the now-redundant CHECK afterwards. Build the index with CREATE INDEX CONCURRENTLY, which takes SHARE UPDATE EXCLUSIVE, makes two passes plus a wait for concurrent transactions, cannot run inside a transaction block, and leaves an INVALID index behind on failure that must be dropped and rebuilt.
Follow-up
- CREATE INDEX CONCURRENTLY has been running for four hours and you need to cancel it. What state is the index left in, how do you detect it, and what do you run next?
- Your backfill is at 40% and the replica is 90 seconds behind. What do you change, and what do you measure to decide the new batch size?
- The code set gains a value two weeks later. Does your CHECK constraint or a lookup table with a foreign key make that change cheaper, and what does each cost on the write path?
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.
Worked solution (20 min)
- Write the two constraints and say which invariant each one holds: the three-column UNIQUE holds version history, the partial unique index holds single-live-decision.
- Write the resolution query filtering on superseded_by_link_id IS NULL plus the two linked statuses.
- Write the collision detector over the existing table and run it mentally against the 004821 case to confirm it returns that value with two authorities.
- Decide the contract for zero rows versus a live 'unlinked' row: zero rows means the source record was never seen, 'unlinked' means it was seen and deliberately not attached, and the caller must handle them differently.
- Write the one-line fix for the broken extract: add the authority column to the extract and to the join, then re-run and diff the row count against the old output.
Expected result: Constraints: UNIQUE (assigning_authority, source_person_id, version) and CREATE UNIQUE INDEX ON person_identity_link (assigning_authority, source_person_id) WHERE superseded_by_link_id IS NULL. Resolution: SELECT l.enterprise_person_id FROM person_identity_link l WHERE l.assigning_authority = :aa AND l.source_person_id = :mrn AND l.superseded_by_link_id IS NULL AND l.link_status IN ('auto_linked','manual_linked'); zero or one row by construction. Detector: SELECT source_person_id, count(DISTINCT assigning_authority) AS authorities, array_agg(DISTINCT assigning_authority) FROM person_identity_link GROUP BY 1 HAVING count(DISTINCT assigning_authority) > 1;
Verify it yourself
- The detector returns 004821 with two authorities, confirming the collision is in the data and not a theory.
- Inserting a second live row for one (authority, MRN) raises a unique violation, while inserting a superseded row for the same pair succeeds.
- The resolution query returns at most one row for every (authority, MRN) pair in the table — run it as a LATERAL over the distinct pairs and assert no count exceeds one.
- Re-running the repaired extract yields a strictly different enterprise_person_id for at least one of the two 004821 patients.
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 many models use in Django?
Medium
Approach
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
- 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?
What specific coding experience do you have with enterprise applications?
Medium
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Shape a bulk enrolment endpoint where rows fail independently
Medium · bulk operations · partial failure · idempotency
A payer partner submits weekly enrolment as one call carrying 10^4 to 2x10^6 member rows that become coverage_span (coverage_id, enterprise_person_id, payer_id, plan_id, subscriber_id, effective_date, termination_date, coverage_order, status, valid_from, valid_to, source_file_id). Some rows fail identity resolution, some carry a termination date before their effective date, and the rest apply cleanly. The partner's loader must be able to retry only what failed, and cannot fix the file until next week. Design the submission contract: synchronous or asynchronous, the response shape, how a row is addressed, and what retrying the failed subset does to rows that already applied.
Approach
- Size the call before shaping it. 2x10^6 rows will not survive a request/response cycle under any sane timeout, so the contract is asynchronous: upload, 202 with a job resource, poll or subscribe. All-or-nothing is equally wrong, because one malformed row would reject two million good ones and the partner cannot resubmit for a week.
- Give every row a caller-assigned stable identity - the partner's file id plus line number - so a result addresses back to the input without positional matching. Positional indexes break the instant the partner resubmits a filtered file, which is exactly what the retry is.
- Make the results a separate paginated resource, not an inline array: GET /enrolment-jobs/{id}/results?status=failed&cursor= returns one entry per failed row with a reason code and the offending field. An inline error array is unbounded, and two million failures is not a response body.
- Classify per row into applied, rejected and needs_review, and keep the last two addressable rather than dropping them. A row that failed identity resolution is not invalid data, it is an unresolved person, and it must be re-drivable after a manual link without the partner resubmitting anything.
- Make the row the unit of idempotency: UNIQUE (source_file_id, partner_line_number), so a resubmitted overlapping file is absorbed. State the one case that is not a duplicate - the same key with changed content must supersede, which in a bitemporal table means closing the prior version's valid_to and inserting a new row with valid_from set, leaving effective_date and termination_date as the partner asserted them rather than rewriting business time.
Worked solution (30 min)
- Decide the transport and show the arithmetic that rules out a synchronous body at two million rows.
- Write the job resource schema: state, counts by outcome, started and finished timestamps, and links to the result pages.
- Write the per-row result schema with the caller's row identifier, the outcome, the reason code and the offending field.
- Write the row-level idempotency key and its constraint, then trace a retry of the failed subset against a job where 90 percent applied.
- Write what happens to a row whose content changed since it applied: the valid_to close-out and the new version, with business dates untouched.
Expected result: POST /enrolment-files returns 202 with Location: /enrolment-jobs/{id}. GET on the job returns {state, counts:{applied, rejected, needs_review}, started_at, finished_at}. GET /enrolment-jobs/{id}/results?status=failed is cursor-paginated, one entry per input line keyed by (file_id, line_number) with reason_code and field. Row idempotency on UNIQUE (source_file_id, partner_line_number), so a retried subset re-applies nothing already applied. A changed row closes the prior version's valid_to and inserts a new version with valid_from, leaving effective_date and termination_date as asserted.
Verify it yourself
- No response in the design grows with the number of input rows.
- Every failed row is addressable by an identifier the partner assigned, not by position.
- Retrying the failed subset twice leaves the same coverage_span state as retrying it once.
- A row rejected for unresolved identity can be re-driven after a manual link without a partner resubmission.
Follow-up
- Midway the job hits a run of failures caused by our own dependency being down. Does the job fail, pause or keep going, and what does the partner see?
- The partner resubmits the entire file instead of the failed subset. What happens?
- How does the partner learn that a row applied and was later superseded by a newer file, without diffing two full exports?
Morning eligibility spike saturates the outbound payer pool
Medium · cache stampede · thundering herd · timeouts
The eligibility service caches answers keyed by (person, plan, service date) with a 24-hour TTL. Every weekday at 07:00 local, for roughly 90 seconds, p99 goes from 45ms to the 6s outbound timeout, the connection pool to external payers saturates, and payers begin returning 429. Cache hit rate dips to 55%. Payer-reported latency for the requests they do serve is unchanged. Name the mechanism, the evidence that separates it from a slow dependency, and the fix — including what you would not do to the TTL.
Approach
- Separate many distinct keys missing from one key missed many times concurrently. Log (key, outcome) and, over a ten-second window inside the spike, compute misses divided by distinct keys missed. A ratio near one is a cold cache and a capacity problem; a ratio of twenty or fifty is a stampede, where concurrent requests for the same key each perform their own origin fetch.
- Confirm the expiry is synchronised. Histogram the remaining TTL across live keys: a single absolute 24-hour TTL set during yesterday's 07:00 peak produces a spike in that histogram exactly 24 hours later, which is why the incident is punctual rather than proportional to load.
- Locate the saturation. Payer-side latency flat while client-observed latency climbs to the timeout means the queueing is on your side. Measure in-flight outbound requests against the pool maximum and the time spent waiting to acquire a connection. That is the evidence that rules out a slow dependency, and the 429s are a consequence of your fan-out rather than its cause.
- Remove the duplication first with single-flight coalescing keyed on (enterprise_person_id, plan_id, service_date): concurrent misses for one key make one origin call and the rest wait on its result. This alone collapses the misses-to-keys ratio and is independent of every other change.
- Then fix the synchronisation and bound the exposure. Use a TTL of minutes with plus or minus 20% jitter so expiries spread, plus a bounded stale-while-revalidate. State the worst case explicitly — a 300s TTL with a 10s stale window is at most 310s of staleness — and return the computed-as-of timestamp to the caller, because coverage terminates retroactively and a long TTL is a correctness decision, not a performance knob.
- Bulkhead the outbound path per payer so one slow or rate-limiting payer cannot consume the concurrency budget of requests destined for the others, and define what the service returns when it cannot reach the origin at all.
Follow-up
- What does the service return when the payer is unreachable and the cache is empty? Does registration proceed with an unverified flag or block, and who owns that decision?
- How would you warm the cache before 07:00 without the warm-up itself becoming the stampede — and which keys are even warmable given the service date is part of the key?
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.
What the behavioural round is actually measuring
Every story you tell gets read for blast radius and judgement: what could have broken, who else it touched, what you knew at the moment you decided. Nobody can audit your code in an hour, so they audit your reasoning instead. Pick work where the call was genuinely yours and the consequences were real enough to remember.
Tell me about yourself and your professional background.
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How did you handle a difficult situation or conflict with a colleague?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- 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.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Tell me about your experience in SQL and database management.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Describe a situation where you had to work as part of a team to complete a project. What was your role, and how did you contribute to the team's success?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?