Interview guides · nib health funds
nib health funds Software Engineer Interview Questions & Guide 2026
nib health funds software engineer prep on operations: decide what you would need to see to know a release is going wrong.
The 60-second brief
A Software Engineer at nib health funds plays a pivotal role in maintaining and evolving the digital infrastructure that connects thousands of members to their health coverage. You are not just writing code; you are building robust, scalable platforms that manage sensitive data and provide seamless user experiences. Your work directly impacts how members interact with their health services, making reliability and security non-negotiable aspects of your daily output.
If the loop includes an asynchronous take-home, treat it as a code review of you rather than as a puzzle: structure, what you chose to test, and what you wrote down about the constraint you were working under. Hold the stated time box and say what you would have done with more of it.
Focus areas
- Version clinical results instead of updating rows in place
- Design person merges that remain reversible afterwards
- Make HL7 and X12 ingestion idempotent under replay
Overview
This role is particularly dynamic due to the intersection of traditional health insurance services and modern SaaS and Technology Platform requirements. You will work within cross-functional teams, collaborating closely with product managers and senior engineering leadership to solve complex technical problems. If you enjoy working on systems that require high availability and are driven by a mission to improve health outcomes through technology, this position offers significant room for professional growth and technical influence.
The nib health funds interview process
nib health funds candidates report 6 rounds · ≈ 4-6 weeks. The stages below are what candidates describe, not a published process.
Initial Screening
The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.
What to demonstrate
- Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
- Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
- Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year
How to prepare
- Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
- Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
- Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter
Technical Evaluations
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
Remote Assessments
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
In-Person/Virtual Interviews
You cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Behavioral Rounds
Many of these questions are about something that went wrong, and the grading sits mostly in the hours after you knew. Who found out first, whether that was you or an alert or a user, how long it took you to say it out loud, and whether the people who needed the news got it while they could still act on it. Engineers under-tell this part because it feels like confessing. The pattern it is looking for is the opposite: the quiet fix, an incident absorbed without telling anyone, after which nothing changed and the same failure is still available.
What to demonstrate
- How the problem was found, and whether that route was one you had built or one that happened to you, since a user reporting it first means your instrumentation did not cover that failure
- Whether time-to-detect and time-to-tell are separate numbers in your account and whether you know both, because a fast fix that nobody heard about until the retro is a different answer from a slow one that was announced immediately
- Whether the resolution left something durable behind, a check that fires or a default that changed, rather than depending on people remembering to be careful
- Whether you can say what the failure cost without either inflating it or waving it away
How to prepare
- Reconstruct one incident you were part of as a timeline with clock times: first bad request, first signal, first person who knew, first message outside the team, mitigation, permanent fix. The gaps between those entries are what gets asked about
- Look up the configuration of the signal that caught it, including its evaluation window and threshold. An alert defined on a five-minute aggregate cannot fire until the condition holds across that window, which puts a floor under time-to-detect that has nothing to do with how severe the failure was. Be able to say what that floor was and whether anyone had chosen it deliberately
- Prepare one story where you escalated early and the severity turned out to be smaller than you thought, including what it cost the people you pulled in. Without it, every answer you give about raising alarms is unfalsifiable
Group Activities
An unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
Where candidates lose points
Writing the access audit record inside the read transaction, or firing it off after the response with no durability.
Inside the transaction, an audit-store outage blocks clinical reads and turns a logging dependency into a care outage. Fire-and-forget afterwards means the audit trail is incomplete during precisely the incidents it exists to reconstruct, and the gaps are invisible until someone asks for the log. The usual resolution is committing the access decision and its audit row together to a local outbox and shipping asynchronously, which keeps the read path available while preserving durability.
Upserting on the order or result identifier, so a correction overwrites the original row.
It makes the question 'what did the clinician see at 14:02' unanswerable, which is exactly what an incident review or a legal hold asks. It also leaves downstream consumers that already acted on the preliminary value with no correction event to react to, because the state transition was collapsed into a single mutated row and never emitted.
Sorting when the problem never required a total order
Match the algorithm to the guarantee actually needed: the top k comes from a size-k heap in O(n log k) time and O(k) space, distinctness needs a set rather than an ordering, and a small bounded integer key range admits a linear counting pass. A full O(n log n) sort is the right default only when you genuinely need everything in order.
Assuming the bug is in the framework
Suspect your own code first: read the stack trace top to bottom, check which versions are actually installed rather than which ones you believe are, and reproduce in isolation before blaming a library that thousands of people run daily. When the fault really is upstream, you need that minimal reproduction to say so credibly anyway.
Questions & practice
Merge twelve resource streams into one patient summary page
Medium · k-way merge · heap · pagination
A patient summary fans out to between 8 and 12 resource types. Each returns a network-backed, paged iterator of resource versions sorted by issued_ts descending, up to 200,000 versions per type for one person. Return the 50 most recent current versions across all types, where current means no later version supersedes it within the same logical resource, and a logical resource whose current version is entered_in_error is omitted entirely. You may not materialise the iterators. Give time and space in terms of k types, the result size and the page size.
Approach
- k-way merge with a max-heap holding one head per iterator, keyed on issued_ts. Seeding is O(k), each pop is O(log k), so reaching R emitted rows costs O(k + P log k) for P pops, with O(k) heap space plus one page buffered per iterator. Fetching everything and sorting is O(V log V) over V up to 2.4 million versions and drags every page across the network to produce 50 rows.
- Suppress with a hash set of logical resource ids already seen, recorded on first sight whether or not that version is emitted. A logical resource has exactly one resource type, so all of its versions arrive on one iterator, and that iterator is descending in issued_ts: the first version you see for a logical resource is its newest. Deciding on first sight and suppressing every later pop for that id is therefore correct in one pass with no lookahead.
- Count emits, not pops. A correction-heavy chart can burn many pops per emitted row, so a loop that stops at 50 pops returns a short page. Put a bound on total pops as well, and when it trips, return what you have with a continuation token rather than spinning.
- Break issued_ts ties deterministically on (resource type, resource id). Without it two identical requests return two different orderings and the next page silently skips or repeats rows.
- Handle entered_in_error at first sight: the erroneous version still supersedes its predecessor, so record the id in the seen set and emit nothing, dropping the whole logical resource instead of falling back to the value it replaced. Recording it is the load-bearing half. Skip it and the next pop re-displays the value a clinician already retracted.
- Summarise the budget honestly: the composite p99 is what the user feels, and it is bounded below by the slowest of the k iterators, so the merge fixes the ordering cost but not the fan-out tail.
Worked solution (25 min)
- Seed the heap with the head of each iterator and an empty emitted-id set.
- Pop the maximum by issued_ts, push that iterator's next head, and check the logical resource id against the set.
- On first sight record the id, then emit unless that version is entered_in_error; on a repeat sight suppress. Stop when emits reach the target.
- Return the rows plus a continuation token carrying the last (issued_ts, type, id) tie-break tuple.
Expected result: Three streams: labs hold A2 at 10:05 which supersedes A1 at 09:00, plus B1 at 08:30; medications hold C1 at 09:30; encounters hold D1 at 10:01. Asking for three rows emits A2, D1, C1 in three pops. Asking for four takes five pops: the fourth pop is A1, suppressed because logical resource A was already seen, and the fifth is B1, emitted. Now mark A2 entered_in_error: asking for three rows takes those same five pops but emits D1, C1 and B1, because the A2 pop records logical resource A and emits nothing, and the A1 pop is then suppressed rather than resurrecting the 09:00 value.
Verify it yourself
- Emitted rows are non-increasing in issued_ts.
- No logical resource id appears more than once in the page.
- Every pop falls into exactly one of three buckets, so pops equal emits plus later versions suppressed plus logical resources dropped as entered_in_error. Pops equal emits only when the popped prefix holds no second version of any logical resource and no first-sight version that is entered_in_error.
- Running with k set to 1 returns that stream's first 50 surviving current versions in exactly 50 pops plus one per later version suppressed plus one per logical resource dropped as entered_in_error.
Follow-up
- One of the twelve iterators has a p99 of 400ms while the rest return in 20ms. What is your composite p99 and what would you change first?
- The user pages to rows 51 through 100. How do you resume without re-reading from the top, and what breaks if issued_ts is not unique?
- One resource type is accidentally returning ascending order. How would your code detect that rather than quietly emitting the oldest rows?
Group transfer-chained encounters into episodes with out-of-order arrival
Medium · graph traversal · cycle detection · memoisation
You have up to 5 million encounter rows for a reporting month, in arbitrary order: encounter_id, enterprise_person_id, facility_id, encounter_class, admit_ts, discharge_ts (nullable), status, prior_encounter_id (nullable, set on transfer), source_event_ts. prior_encounter_id may reference a row that arrives later or never arrives. Group rows into episodes, one per maximal transfer chain, returning episode root, person, earliest admit_ts, latest discharge_ts (null if any member is still open) and member count. Report cycles instead of following them. Target O(N) expected time.
Approach
- Recognise the shape: every row has at most one prior_encounter_id, so this is a forest of parent pointers, not a general graph. A hash map from encounter_id to row is the whole index you need, and the work is O(N) expected rather than anything sort-shaped.
- Resolve roots by iterative pointer-following with write-back memoisation: from each node walk prior_encounter_id until you reach a node with no parent, a parent absent from the input, or a node whose root is already known, then stamp the discovered root onto every node on that path. Each node is finalised once, so total work stays O(N) even when one chain is very long.
- Do not recurse. A malformed feed can produce a chain tens of thousands deep, and a recursive resolver dies on stack depth taking the whole batch with it. An explicit loop with write-back is both faster and bounded.
- Detect cycles with three-state marking: unvisited, on the current path, finalised. On reaching a node that is on the current path, emit a data-quality record naming every encounter_id in the cycle and exclude that component from the episode output. Choosing an arbitrary root instead would produce an episode that looks plausible and will be believed.
- Keep a dangling prior_encounter_id distinct from a genuine root. It means the chain starts outside the window or has not arrived, so flag the episode left-truncated and carry that flag into the output: an episode with an unknown start cannot be used for length of stay or readmission counting without biasing both downward.
- Compute aggregates during the same pass that assigns roots: minimum admit_ts, maximum discharge_ts with null propagation so any open member leaves the episode open, and exclude cancelled and entered_in_error members from the aggregates while keeping them retrievable.
Follow-up
- Two encounters name each other as prior_encounter_id. What does your detector emit, and who needs to see it?
- One episode spans two facilities whose clocks differ by four seconds. Do you order on source_event_ts or admit_ts, and what does the choice cost?
- A chain is cut in half by the month boundary. What does this month's report say, and how do you make next month's agree with it?
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?
Fix a paid-amount report that double counts claim versions
Medium · join fan-out · versioned claims · aggregation correctness
claim_line: claim_line_id, claim_id, claim_version smallint, line_number, frequency_code in ('original','replacement','void'), enterprise_person_id, coverage_id, billing_provider_npi, service_from_date, procedure_code, units, billed_amount_cents bigint, allowed_amount_cents, paid_amount_cents, adjudication_status, UNIQUE (claim_id, claim_version, line_number). A monthly report sums paid_amount_cents and joins coverage_span on enterprise_person_id with date overlap to attribute a plan. It overstates payment by roughly 9%, and reconciliation against remittance fails. Name both causes, then write the corrected query returning paid cents and plan per billing_provider_npi for March 2026 service dates.
Approach
- Cause one: the SUM spans every claim version. A replacement supersedes a prior claim and a void cancels one, so a replaced claim contributes twice and a voided claim contributes once when it should contribute nothing. Resolve the surviving version per claim_id before aggregating, never by deleting superseded rows.
- Cause two: the coverage join is one-to-many. coverage_span carries a system-time version per coverage period, so a person with three beliefs about one period multiplies every claim line by three. The join is for a label, not for filtering, so it must not be allowed to change cardinality.
- Collapse the version chain with DISTINCT ON (claim_id) ... ORDER BY claim_id, claim_version DESC, then join the lines back on (claim_id, claim_version), and exclude the surviving version when its frequency_code is 'void'.
- Make the coverage lookup cardinality-safe with a LEFT JOIN LATERAL ... LIMIT 1 pinned to valid_to = 'infinity' and ordered by valid_from DESC. LATERAL with LIMIT 1 cannot fan out by construction; a plain join can, and no amount of later DISTINCT repairs a SUM that already doubled.
- Keep the money in integer minor units the whole way. SUM(bigint) returns numeric in PostgreSQL, so there is no overflow and no binary floating-point error; cast to a display type only at the edge.
Follow-up
- A replacement arrives before its original. Your surviving-version logic picks max(claim_version) — what does the report show in the window before the original lands, and does the number self-correct?
- Add a claim_line_adjustment table with several reason codes per line. What does that do to this query, and where do you aggregate to keep it safe?
- Reconciliation is still off by 400 cents. Which of the two causes could still produce that, and what is your next query?
Return the displayable version of each analyte with a window function
Medium · window functions · versioned rows · indexing
observation_result is append-only: observation_id, enterprise_person_id, encounter_id, accession_id, placer_order_id, filler_order_id, loinc_code, value_numeric, unit_ucum, result_status in ('registered','preliminary','final','amended','corrected','entered_in_error'), collected_ts, issued_ts, version, supersedes_observation_id. A chart view needs the currently displayable value for each (accession_id, loinc_code) for one person over the last 90 days, excluding analytes whose newest version is entered_in_error but keeping analytes whose older versions were. Write the query, give the index that serves it, and say honestly which part of the plan the index cannot remove.
Approach
- Recognise the shape: this is a top-1-per-group over a version chain, not a filter. Pick the newest version first, then apply the status predicate to the winner — the order is the whole exercise.
- Use DISTINCT ON (accession_id, loinc_code) with ORDER BY accession_id, loinc_code, version DESC, observation_id DESC on PostgreSQL; the ORDER BY must start with the DISTINCT ON expressions or it is a syntax error. Use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY version DESC, observation_id DESC) with an outer WHERE rn = 1 if you need portability, since a window function cannot be referenced in the WHERE of its own query level.
- Push only person and time into the inner WHERE. Pushing result_status <> 'entered_in_error' inside promotes a retracted result's predecessor back onto the chart, which is the bug the prompt is testing for.
- Index (enterprise_person_id, collected_ts DESC) so the 90-day slice is an index range scan rather than a scan of the person's whole history.
- Be straight about the limit: because collected_ts carries a range predicate, no b-tree index can also deliver rows pre-ordered by (accession_id, loinc_code, version DESC), so the sort stays in the plan. That is acceptable because one person's 90-day slice is hundreds to low thousands of rows, and it is a quicksort in work_mem rather than an external merge — verify that rather than assume it.
Worked solution (30 min)
- Write the DISTINCT ON form with the full sort key and only person plus time in the inner WHERE.
- Wrap it in an outer query and apply the entered_in_error filter there, so it can only eliminate winners.
- Write the ROW_NUMBER equivalent and confirm both return identical rows against a fixture containing one preliminary, one final, one corrected and one retracted chain.
- Add the index, then read EXPLAIN (ANALYZE, BUFFERS) and confirm an index scan feeding a Sort node, with Sort Method reported as quicksort and no Disk usage.
- Construct the adversarial fixture: an analyte whose version 1 is entered_in_error and whose version 2 is final. It must appear in the output; an analyte whose version 2 is entered_in_error must not.
Expected result: SELECT * FROM ( SELECT DISTINCT ON (o.accession_id, o.loinc_code) o.accession_id, o.loinc_code, o.value_numeric, o.unit_ucum, o.result_status, o.collected_ts, o.issued_ts FROM observation_result o WHERE o.enterprise_person_id = :p AND o.collected_ts >= now() - interval '90 days' ORDER BY o.accession_id, o.loinc_code, o.version DESC, o.observation_id DESC ) latest WHERE latest.result_status <> 'entered_in_error'; with CREATE INDEX ON observation_result (enterprise_person_id, collected_ts DESC). The plan is an index scan on that index feeding a Sort feeding Unique; the sort cannot be indexed away because collected_ts carries a range predicate.
Verify it yourself
- Output row count equals SELECT count(DISTINCT (accession_id, loinc_code)) over the same window minus the number of chains whose newest version is entered_in_error.
- Every returned observation_id has no other row pointing at it via supersedes_observation_id within the window.
- All versions of one analyte share the same collected_ts; if they do not, the 90-day window can cut a version chain in half and return a stale winner.
- EXPLAIN (ANALYZE) reports Sort Method: quicksort with Memory in kilobytes, not external merge Disk — if it spills, the per-person slice is larger than assumed and the design needs revisiting.
Follow-up
- A correction arrives carrying the same filler_order_id and the same version number as the row it corrects. What breaks, and what do you add to the sort key?
- The same query for a cohort of 50,000 people instead of one person: does DISTINCT ON still hold up, and at what point do you materialise a latest-version table with a partial index?
- How would you show the clinician that the displayed value was amended, given the chart query returns only the winner?
Walk us through a technical assessment you have completed recently.
Medium
Approach
- Work from the requirement backwards to the design.
- 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
- What assumption would you test first?
- How would you know your answer was wrong?
Benefit accumulators under concurrent claim adjudication
Medium · isolation · lock-contention · partitioning · retries
Adjudication applies benefit by reading a member's remaining deductible and out-of-pocket accumulators, computing patient responsibility, then writing both the new accumulator values and the claim_line amounts (allowed_amount_cents, paid_amount_cents, patient_responsibility_cents, all bigint minor units). Several lines for one member adjudicate concurrently out of a batch of a few million lines a day, alongside a real-time path. On PostgreSQL at READ COMMITTED, exhibit the interleaving that overshoots the plan limit, then give three mechanisms that prevent it and the throughput each one costs.
Approach
- Exhibit the anomaly with numbers. Transactions A and B each SELECT remaining_deductible = 40000 cents, each compute an application of 30000, each UPDATE to 10000, and both commit: 60000 applied against a 40000 limit. The lost update lives across the statement boundary — READ COMMITTED does re-evaluate a blocking UPDATE's own predicate against the newly committed row, but it does nothing about a value the application read in an earlier statement.
- Mechanism one, SELECT ... FOR UPDATE on the accumulator row at the top of the transaction: correct, simple, and it serialises that one member's lines. The cost is a held row lock for the transaction's lifetime, so contract and pricing lookups must complete before the lock is taken — a lock held across a call to an external system is how one slow partner stalls a queue.
- Mechanism two, SERIALIZABLE with a bounded retry on SQLSTATE 40001: no explicit locking, but the abort rate climbs with contention and each retry redoes the whole computation, so effective throughput falls as contention rises rather than degrading gently. The retry is only safe if the transaction is idempotent or can recognise its own partial work by a claim-version key.
- Mechanism three, partition the work queue by enterprise_person_id so one member is handled by exactly one writer: contention disappears instead of being resolved, at the cost of a rebalancing story and a real-time request queueing behind the batch for a hot member.
- Handle reversals, where all three are equally exposed: the amount to subtract is what was applied at the time under the plan design then in force, not what recomputation today would produce. Record the applied amount on the claim version and subtract that stored value.
- Choose and justify for this shape: batch dominates and members are naturally independent, so partition by member for the batch path while the real-time path takes FOR UPDATE on the same row, since both must respect one counter.
Worked solution (30 min)
- Open two psql sessions and reproduce the lost update against a real accumulator row, recording the exact statement order.
- Rewrite the path three ways — FOR UPDATE, SERIALIZABLE with retry, per-member partition — and run the same two-session interleaving against each.
- Measure or estimate throughput per member for each variant and write one paragraph on where each degrades.
- Write the reversal path and state which stored value it subtracts and why recomputation is wrong.
- Mark every point in the transaction where an external call would extend a held lock.
Expected result: A concrete lost-update interleaving under READ COMMITTED, three mechanisms with their distinct costs — held-lock duration, retry amplification under contention, per-member throughput ceiling — a reversal that subtracts the recorded applied amount rather than a recomputed one, and a stated choice with its reasoning.
Verify it yourself
- The lost update reproduces in two real sessions, not only on paper.
- Under each of the three mechanisms, a hundred concurrent applications against a 40000-cent limit leave the accumulator at exactly 40000 and never above.
- A retried transaction under SERIALIZABLE applies the benefit once, verified by the claim version rather than by the accumulator alone.
- No transaction in any variant holds a lock across a network call.
Follow-up
- Eligibility changes retroactively, so a claim adjudicated in March should have denied. What must be recomputed, in what order, and what happens to the accumulators in between?
- A family deductible accumulates across several members' claims simultaneously. What is the lock now, and where is the new deadlock?
Nightly adjudication throughput collapses with deadlocks on accumulators
Medium · deadlock · lock ordering · isolation levels
During the nightly claims batch, throughput collapses from 4,000 to about 300 claim lines a second for minutes at a time. pg_stat_database.deadlocks climbs, the PostgreSQL log carries deadlock detected with SQLSTATE 40P01, and the application retries the aborted transactions. Benefit application and the reversal handler both take SELECT ... FOR UPDATE on the member's deductible accumulator row and on the out-of-pocket accumulator row. Diagnose in order, then give a fix that removes the cycle by construction rather than by retrying faster.
Approach
- Read the deadlock report before theorising. PostgreSQL logs both process IDs, the statement each was running and the relation and tuple each was waiting on, which names the two lock-acquisition orders directly. There is no need to guess which code paths collide or to reproduce it first.
- Separate deadlock from ordinary contention. pg_stat_database.deadlocks counts cycles, while log_lock_waits reports waits longer than deadlock_timeout that never form a cycle. Collapsing throughput with few recorded deadlocks means a hot row — one family plan's shared accumulator — is serialising the batch, which has a different remedy from a cycle.
- Account for the detector's cost. A cycle is only detected after deadlock_timeout, one second by default, so every deadlock burns at least that on both sides plus the retried work. Raising the timeout lengthens each stall; lowering it raises the frequency of the detector's checks. Neither is a fix, and both are common first answers.
- Reject the two reflex answers explicitly. SERIALIZABLE does not prevent deadlocks in PostgreSQL — row-level lock cycles still occur and serialization failures with SQLSTATE 40001 are added on top — and a larger connection pool simply puts more transactions in contention for the same rows.
- Remove the cycle by construction: make each member's benefit application take exactly one lock. Either fold both accumulators into a single row keyed (enterprise_person_id, plan_id, plan_year) and update it in one statement, or take pg_advisory_xact_lock over a hash of that key as the first statement of both paths. One lock cannot form a cycle with itself. Deterministic ordering via ORDER BY ... FOR UPDATE also works but depends on the chosen plan preserving that order, so it is a weaker guarantee; hash collisions in the advisory-lock variant only serialise two unrelated members, which is safe but costs throughput.
- Then bound the transaction. No external call may sit between acquiring the lock and committing, or the row is held for a network round trip and the hot member serialises the batch regardless of which deadlock fix you chose.
Follow-up
- A reversal arrives for a claim adjudicated under a plan design that has since changed. What amount does it subtract, where is that amount recorded, and is that still atomic under your single-lock design?
- Remove the FOR UPDATE entirely and show the exact two-session interleaving that produces a lost update under READ COMMITTED, then say why a single UPDATE ... SET remaining = remaining - $1 does not have the same problem.
Suggested practice checklist
Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.
Day 1 — Coding, one pass at shallow depth
- Solve one problem from each of six families, an array with two pointers, hash counting, binary search, a tree traversal, a graph traversal and one dynamic program, under a hard twenty-minute cap with no extensions, marking each finished, late, or stalled.
- For every stall, write the exact move you could not make rather than the subject, so the note reads could not turn the recurrence into a loop rather than bad at dynamic programming.
- Fix nothing today. The value of the pass is the unfixed record.
Deliverable: Six timed attempts marked finished, late or stalled, each stall carrying a named blocking move.
Day 2 — Design, one pass at shallow depth
- Spend twenty minutes each on three different shapes, a read-heavy feed, a write-heavy ingest path, and something needing a transaction across two entities, stopping each at requirements, interface and data model.
- After each, write the first question you could not answer, which is usually a number you could not estimate or a failure mode you had no vocabulary for.
- Mark which of the three you would be most relieved not to be asked, and treat that as data rather than as a preference.
Deliverable: Three shallow designs, each with the first unanswerable question written at the bottom.
Day 3 — Fundamentals and the practical rounds
- Answer eight short questions in writing at four minutes each, covering the material that fills the gaps between the big rounds: what happens between a URL and a rendered page, what an index costs on write, when a process is preferable to a thread, and what conditions a deadlock requires.
- Do one thirty-minute practical task of the kind a take-home compresses: read an unfamiliar two-hundred-line file and write what it does, what you would change, and the one thing you remain unsure of.
- Score every answer fluent, correct but slow, or absent, and keep the absent ones visible.
Deliverable: Eight scored short answers and one written reading of unfamiliar code.
Day 4 — The rounds that are about you, and the map
- Deliver three behavioural answers aloud against a timer, a conflict, a failure you owned, and a decision made without enough information, marking any that ran past three minutes or contained no number.
- Assemble the map: every marked item from days one to three on a single page, sorted by how likely it is to appear in your loop rather than by how uncomfortable it felt.
- Choose exactly two areas for the remaining three days and write down what you are deliberately abandoning.
Deliverable: A one-page scored map of the whole surface area with two areas chosen and the rest explicitly abandoned.
Day 5 — First chosen area, to the depth you skipped
- Work the higher-ranked area in four focused blocks, choosing items one level above where you stalled rather than repeating what already works.
- After each block write the rule you extracted in one sentence with its precondition attached, since a rule carrying no precondition is exactly what fails under a variation.
- Re-attempt the day-one or day-two item that exposed this area and compare against the original timing.
Deliverable: Four worked blocks, a timed re-attempt against the original, and three one-sentence rules with preconditions.
Day 6 — Second chosen area, where the gap is coverage rather than speed
- Treat the second area differently from the first. Day five drilled something you could already half-do; this one is usually a topic you had simply never met, so build one worked reference example end to end and keep it, rather than attempting six problems badly.
- Write down the vocabulary you were missing on day two or three, five terms at most, each with the one sentence that makes it usable in an answer rather than the textbook definition.
- Redo the shallow attempt that exposed this area and note whether you now fail later in the problem, because moving the failure point is the realistic gain from a single day and is worth more than a score that did not change.
Deliverable: One worked reference example for the newly covered area, a five-term vocabulary list, and a note on where the failure point moved.
Day 7 — Reassemble the loop
- Sit two rounds back to back with no gap, ordering them so the area you chose second comes last, because the map was built from rested, isolated attempts and the loop will reach your weaker area when you are already spent.
- Write where the second round suffered from the first, which is normally the point at which structure collapses into narration.
- Reduce the week to one page holding only the rules you can state without reading them.
Deliverable: Mock notes on cross-round carryover plus a one-page card of rules you can recite from memory.
Know where your numbers came from
Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.
Why did you apply to this specific role at nib health funds?
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.
- 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 would you do differently if you ran that again?
Explain a time you had to navigate a difficult situation at work.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- 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
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Tell us about a time you made a mistake and had to take steps to rectify it.
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 assist a fellow employee; what was the outcome?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- 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
- How did you know your change caused the improvement?
- What did you decide not to do, and why?