cswork / libraryOpen practice workspace

Interview guides · Bamboo Health

Bamboo Health Software Engineer Interview Questions & Guide 2026

Software engineer prep for Bamboo Health: design exercises weighted toward the constraints of healthcare and life sciences work.

The 60-second brief

As a Software Engineer at Bamboo Health, you are at the intersection of complex healthcare data and actionable clinical insights. This role is fundamental to the company’s mission of delivering real-time, data-driven solutions that connect care providers, payers, and patients across the continuum of care. You will contribute to the development of software that helps manage patient transitions, reduce readmissions, and improve overall health outcomes.

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

  • Make HL7 and X12 ingestion idempotent under replay
  • Model bitemporal coverage and retroactive eligibility changes
  • Authorise every identified read against current consent

Overview

The work is intellectually demanding, requiring you to navigate large-scale data systems and ensure high reliability in environments where information accuracy directly impacts patient care. You will collaborate with cross-functional teams, including product managers and clinical experts, to translate complex healthcare requirements into robust, scalable technical solutions. This is an environment for engineers who value technical precision, system design, and the tangible impact their code has on the healthcare ecosystem.

The Bamboo Health interview process

Bamboo Health candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Recruiter Screen

Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.

What to demonstrate

  • Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
  • Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
  • Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not

How to prepare

  • Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
  • Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
  • If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window

Practical Assessment

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

Technical Discussions

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

Panel Interview

Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.

What to demonstrate

  • Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
  • Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
  • Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
  • Whether you can say which calls you made alone and which you escalated, and why the line sat where it did

How to prepare

  • Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
  • Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
  • Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap

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.

Caching an eligibility answer with a long time-to-live and without an as-of date.

Coverage terminates retroactively as a matter of routine: an enrolment file received on the fifth of the month can terminate coverage effective the first. A day-long cache means services are delivered against a 'covered' answer that was already false when it was served, and the denial arrives weeks later. The answer needs to be keyed on person, plan and service date, carry the date it was computed as of, and expire fast enough that the exposure window is a decision rather than an accident.

Hardcoding to the sample inputs

Solve the stated problem rather than the two examples; special-casing a literal to make a sample pass is obvious immediately and reads as either a misunderstanding or an attempt to fake progress. If you genuinely cannot generalise yet, say which part is a stub and what would replace it.

Abandoning working code to chase the optimal solution

Get the straightforward version correct, state its complexity, and only then optimise, keeping the working version until the faster one passes the same cases. A correct quadratic solution with a stated path to linear beats a half-written optimal one that never ran.

Questions & practice

Extract an idempotency key from a raw HL7v2 message

Easy · parsing · idempotency · delimiters

An HL7v2 message arrives as a byte buffer up to 256 KB, segments terminated by carriage return (0x0D), first segment MSH. MSH-1 is the field separator character itself and MSH-2 holds the encoding characters, so splitting the MSH segment on the separator puts MSH-n at index n-1 for n of 2 or more. Return the idempotency key built from MSH-4 sending facility, MSH-3 sending application, MSH-10 message control ID and MSH-7 event timestamp, with escape sequences decoded in each value. Single pass, O(n). Do not hardcode the separator characters.

Approach

  1. Read the delimiters out of the message instead of assuming them: the byte immediately after MSH is the field separator, and the next field's bytes give component, repetition, escape and subcomponent separators in that order. Everything downstream uses those values, because a partner is entitled to send different ones and the common defaults are a convention, not a guarantee.
  2. Verify the first three bytes are MSH before anything else and reject otherwise, since a socket read can begin mid-message. Then bound the segment at the first terminator and split that slice only, applying the n-1 offset that the MSH segment alone requires.
  3. Decode escapes in one left-to-right pass per extracted value: on the escape character, read to the next escape character and map the codes for field, component, subcomponent, repetition and escape back to their literal characters. Treat an unterminated escape as a malformed message rather than dropping the tail silently.
  4. Normalise MSH-7 to UTC before it enters the key. The timestamp may carry an offset or omit one, and two spellings of the same instant must hash identically or the ledger stops deduplicating the moment a partner changes its formatter.
  5. Compose the key as the full tuple, not the control ID alone. The control ID is unique only per sending application and some senders roll the counter over, so facility plus application plus control ID plus instant is the smallest key that survives a rollover.
  6. Cost is O(n) time and O(k) space for the extracted values. Hashing the whole body is also O(n) but cannot distinguish a genuine resend from a corrected retransmission that reuses the control ID, which is a different event.

Follow-up

  • One partner sends MSH-7 with no timezone offset and another sends it with an offset. What do you store, and what do you compare on?
  • A sender rolls its control ID counter over and restarts from zero. Which component of your key absorbs that, and which message pairs would still collide?
  • Segments arrive separated by line feed instead of carriage return, or with a trailing empty field. Which should the parser accept and which should it negatively acknowledge?

Flag a requester reading too many distinct charts per window

Medium · sliding window · two pointers · distinct count

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

Approach

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

Worked solution (25 min)

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

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

Verify it yourself

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

Follow-up

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

Sum surviving claim versions in one pass over unordered lines

Easy · hash map · versioning · streaming aggregation

You are streamed up to 50 million claim_line records in arbitrary order: claim_id, claim_version, line_number, frequency_code (original, replacement, void), enterprise_person_id, allowed_amount_cents. Every version of a claim shares its claim_id, and a replacement arrives as a higher claim_version. Return total allowed_amount_cents per enterprise_person_id, counting each claim once at its highest version and contributing zero when that version is a void. Amounts are non-negative int64 minor units. Target O(N) time and O(C) space for C distinct claims, single pass, no sort.

Approach

  1. Say the two grains out loud before writing anything: the version lives at claim grain, the money lives at line grain. Every wrong answer here comes from applying a claim-level rule with a line-level filter.
  2. Keep one hash map from claim_id to a small record (best_version, sum_cents, person_id, is_void). Per line: if claim_version is greater than best_version, reset sum_cents to this line's amount and overwrite the person and void flag; if equal, add to it; if lower, discard the line. That reset is what makes the pass order-independent, so a replacement arriving before its original still wins.
  3. Resolve the void at the end, not at ingest. A void only nullifies the claim if it is the surviving version, and zeroing on sight would let an earlier replacement that arrives later resurrect the money.
  4. Fold the C claim records into a person-keyed map in a second phase, O(C). Do not accumulate into the person total during the stream: you cannot subtract a superseded version you have already forgotten.
  5. Cost is O(N) time, O(C) space. The sort-based alternative, group by (claim_id, claim_version) and keep the max, is O(N log N) and needs the set resident; prefer it only when C approaches N and the map will not fit.
  6. Keep cents in int64 throughout. Fifty million lines at a realistic per-line ceiling stay four orders of magnitude below 2^63, so the integer type costs nothing and removes the float drift class entirely.

Follow-up

  • You shard the stream across eight workers. Sharding by hash of claim_id works; what exactly breaks if you shard by enterprise_person_id instead?
  • A replacement arrives for a claim_id whose original never appears in the batch. What does your map produce, and is that the financially correct answer or a reconciliation break?
  • The same claim_id is reused by two different submitting organisations. How does the key have to change, and when would you have noticed?

Reproduce and fix a lost update on a deductible accumulator

Medium · isolation levels · lost update · lock contention

An accumulator row holds deductible_applied_cents bigint and plan_deductible_cents bigint, keyed by (enterprise_person_id, plan_id, benefit_year). The adjudicator opens a transaction, SELECTs the row, computes the member's share in application code, then UPDATEs the row to the absolute new total it computed, all under PostgreSQL's default READ COMMITTED. Two claim lines for one member adjudicate concurrently: both charge the member deductible, but the accumulator advances by only one of the two amounts, so the member is billed deductible again on a later line after the plan deductible has already been met. Write the exact two-session interleaving that produces it. Then give three fixes, each naming the lock or isolation level, the SQLSTATE you must handle, and the throughput cost.

Approach

  1. State the anomaly precisely. READ COMMITTED takes a fresh snapshot per statement, so it prevents dirty reads but permits a lost update when a transaction reads a value, computes outside the database, and writes back an absolute result. The vulnerable window is the round trip through application code between two statements, not the transaction boundary — the same logic expressed as one relative UPDATE is safe at this very isolation level.
  2. Write the interleaving as an ordered script both sessions can be replayed from, with the commit points marked, and make both writes absolute (SET deductible_applied_cents = :computed_total). The point is not that two writes happen — it is that the second write stores a total computed from a value that had already been superseded by the time it landed.
  3. Fix one, SELECT ... FOR UPDATE on the read: the second session blocks on the row lock and, under READ COMMITTED, re-reads the newest committed version when it unblocks, so its computation starts from the winner's total. No serialization error to handle. Cost is serialised throughput per member and a lock held for the transaction's whole duration, so nothing inside may call an external payer.
  4. Fix two, REPEATABLE READ or SERIALIZABLE with a retry loop: in PostgreSQL, REPEATABLE READ aborts the second writer of a row with SQLSTATE 40001, 'could not serialize access due to concurrent update'; SERIALIZABLE additionally aborts on read/write dependencies detected by SSI, also under 40001. The retry re-runs the whole transaction, so the claim application must be idempotent or keyed by claim_line_id, or a retry double-applies.
  5. Fix three, single-writer partitioning: route by hash(enterprise_person_id) to one consumer per partition, so no two transactions ever touch one accumulator. No locks, no retries; the cost is head-of-line blocking behind a slow claim, rebalancing during deploys, and losing the ability to scale a hot member.
  6. Name the cheapest option and its real catch. A single UPDATE ... SET deductible_applied_cents = LEAST(plan_deductible_cents, deductible_applied_cents + :amt) ... RETURNING deductible_applied_cents does not lose the update: under READ COMMITTED an UPDATE that hits a concurrently updated row blocks, then re-evaluates its expressions against the newly committed version, so both increments land. The catch is that the member's share is the delta this statement actually applied, which is the returned value minus the row's pre-image — and RETURNING does not expose the pre-image before PostgreSQL 18's OLD/NEW aliases. On earlier versions you must read that pre-image under the same row lock, which puts you back in fix one with a shorter critical section. The one-statement form is a clean fix only when the caller does not need the delta.

Follow-up

  • A claim is reversed six months later under a plan design that has since changed. What amount does the reversal subtract, and where is that number stored?
  • Your retry loop hits 40001 repeatedly for one member during a batch window. What is the backoff, and at what point do you stop retrying and pend the claim?
  • Which of the three fixes survives a retroactive eligibility change that invalidates everything applied in the last month, and what does the rebuild look like?

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

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

Worked solution (35 min)

  1. Prove cause one with a diagnostic: SELECT claim_id, count(DISTINCT claim_version) FROM claim_line GROUP BY 1 HAVING count(DISTINCT claim_version) > 1 — confirm the affected claims exist and their paid totals.
  2. Prove cause two by running the original report with count(*) alongside the SUM and comparing it to the count of qualifying claim_line rows; any excess is fan-out.
  3. Write the surviving-version CTE with DISTINCT ON (claim_id).
  4. Join the lines to it on both claim_id and claim_version, exclude 'void', filter adjudication_status = 'paid' and the half-open March service-date range.
  5. Attach the plan label through LEFT JOIN LATERAL with LIMIT 1, then re-run the count(*) check and confirm it now equals the qualifying line count exactly.
  6. Diff the new total against remittance and confirm the 9% gap closes to zero cents, not to 'close enough'.

Expected result: WITH surviving AS ( SELECT DISTINCT ON (claim_id) claim_id, claim_version, frequency_code FROM claim_line ORDER BY claim_id, claim_version DESC ) SELECT cl.billing_provider_npi, cov.plan_id, SUM(cl.paid_amount_cents) AS paid_cents FROM claim_line cl JOIN surviving s ON s.claim_id = cl.claim_id AND s.claim_version = cl.claim_version LEFT JOIN LATERAL ( SELECT cs.plan_id FROM coverage_span cs WHERE cs.enterprise_person_id = cl.enterprise_person_id AND cs.valid_to = 'infinity' AND cs.effective_date <= cl.service_from_date AND (cs.termination_date IS NULL OR cs.termination_date >= cl.service_from_date) ORDER BY cs.coverage_order, cs.valid_from DESC LIMIT 1 ) cov ON true WHERE s.frequency_code <> 'void' AND cl.adjudication_status = 'paid' AND cl.service_from_date >= DATE '2026-03-01' AND cl.service_from_date < DATE '2026-04-01' GROUP BY 1, 2;

Verify it yourself

  • Replacing the aggregate with count(*) yields exactly the number of claim_line rows that pass the same predicates without the coverage join — equality proves the LATERAL did not fan out.
  • Removing the LATERAL entirely changes no monetary total, only the plan column; if the total moves, the join is still multiplying.
  • Every claim_id appears with exactly one claim_version in the surviving CTE: count(*) equals count(DISTINCT claim_id).
  • The corrected total reconciles to the remittance file to the cent, and the residual for each voided claim is exactly its previously included paid amount.

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?

How do you approach the SDLC (Software Development Lifecycle) when working on new features?

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Say what you would check first and why it is the highest-information step.
  3. Work from the requirement backwards to the design.

Follow-up

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

Why did you choose specific programming languages or frameworks for your past projects?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  2. State your assumptions explicitly before working the problem.
  3. Work from the requirement backwards to the design.

Follow-up

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

What is your process for debugging complex, intermittent issues in a production environment?

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. Say what you would check first and why it is the highest-information step.
  3. State your assumptions explicitly before working the problem.

Follow-up

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

Fix a claim submission endpoint that duplicates on client retry

Medium · idempotency · retries · at-least-once

A billing partner POSTs to /claims over a flaky link and retries on timeout, connection reset and 5xx. Submissions land in claim_line (claim_line_id, claim_id, claim_version, line_number, frequency_code, enterprise_person_id, coverage_id, billing_provider_npi, billed_amount_cents, adjudication_status, UNIQUE (claim_id, claim_version, line_number)). Reconciliation against the remittance file shows some claims adjudicated twice. The partner's own control identifier is unique only within that partner and is reused after a counter rollover. Redesign the contract so a retried submission cannot create a second financial fact, and name the mechanism that still holds with two workers processing concurrently.

Approach

  1. Name why a plain POST cannot survive this. POST is not idempotent under HTTP semantics, and the failure the partner retries on is a lost response, which at the client is indistinguishable from a lost request. The claim was written; only the acknowledgement died.
  2. Choose a key the sender can reproduce on the retry and that is unique in our namespace: a caller-supplied Idempotency-Key scoped to (authenticated partner, endpoint, key). Do not derive it from the partner's control identifier, which rolls over. If a body hash is used instead, hash canonicalised business fields rather than raw bytes, because a proxy that re-serialises JSON changes the key without changing the claim.
  3. Make the deduplication atomic rather than check-then-insert. Under READ COMMITTED two workers can both SELECT 'no such key' and both INSERT, because the check and the write are separate statements. The guard is UNIQUE (partner_id, idempotency_key) with INSERT ... ON CONFLICT DO NOTHING; the loser reads the winner's row. The unique index is what serialises them, across connections and processes, where an application-level check does not.
  4. Define all three replay outcomes, since 'return the same response' is underspecified: a completed key replays the stored status and body with a header marking it a replay; an in-flight key gets 409 with Retry-After and never a second execution; the same key with a different canonical body gets 422, because that is a caller bug and not a retry.
  5. State key retention and the floor underneath it. Keys expire - 24 hours suits a batch partner - and past expiry the business constraint UNIQUE (claim_id, claim_version, line_number) still rejects the duplicate. The idempotency key bounds the cheap window; the business key is the guarantee. Also write the retry classification the partner implements, and note that it is not "never 4xx": transport errors and 5xx retry on exponential backoff with full jitter; 408 and 429 are the two retryable 4xx, with 429 waiting the Retry-After the server sends rather than the backoff schedule; every other 4xx, 400, 403, 404, 409 and 422 among them, is terminal, because a retry reproduces the same caller fault.

Worked solution (30 min)

  1. Write the timeline that produces the duplicate - write committed, response lost, client retries, second write - and mark the exact point at which the server could have known.
  2. Write the DDL for the idempotency record: the unique constraint, the stored status and body, the canonical request fingerprint, the state, and the expiry.
  3. Write the handler as one INSERT ... ON CONFLICT DO NOTHING RETURNING, then the branch for each of the three conflict outcomes.
  4. Run two sessions concurrently against a real database on the same key and confirm exactly one performs the work.
  5. Write the contract paragraph the partner implements: which key, its lifetime, which statuses are safe to retry, and the backoff.

Expected result: POST /claims with a required Idempotency-Key scoped to (partner_id, key), backed by UNIQUE (partner_id, idempotency_key) and a single INSERT ... ON CONFLICT DO NOTHING that picks the winner. Completed key replays the stored 201 body with Idempotent-Replay: true; in-flight returns 409 with Retry-After; same key with a different canonical body returns 422. Keys retained 24 hours, after which UNIQUE (claim_id, claim_version, line_number) still rejects the duplicate. Retryable: transport errors and 5xx on exponential backoff with full jitter, plus 408 and 429, where 429 honours Retry-After; all other 4xx are terminal.

Verify it yourself

  • Two concurrent workers on one key produce exactly one set of claim_line rows.
  • A replay is byte-identical to the original response apart from the replay header.
  • A retry after key expiry is rejected by the business constraint, not silently accepted.
  • Nothing in the design depends on the partner's control identifier being unique.

Follow-up

  • The partner retries with a fresh key but the identical claim. What stops the duplicate now?
  • The stored-response record is a second write in the same request. How do you make it and the claim commit together?
  • A replacement claim arrives before the original it replaces. What does the endpoint return, and what must not happen to the totals?

Gateway instances OOM every five days with flat traffic

Medium · memory leak · heap profiling · idempotency

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

Approach

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

Follow-up

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

Suggested practice checklist

Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.

Day 1 — Diagnostic, scored before you study anything

  • Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
  • Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
  • Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.

Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.

Day 2 — Largest gap: find the boundary rather than the subject

  • Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
  • Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
  • Re-attempt one of them from blank four hours later with nothing open.

Deliverable: A sub-skill map with the two blocking sub-skills circled.

Day 3 — Drill the blocking sub-skill by repeating the shape

  • Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
  • State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
  • Have someone else read your one-sentence rule and find the precondition you left out.

Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.

Day 4 — Second gap, plus maintenance on the strongest area

  • Run the same sub-skill decomposition on the second-largest gap in half the time.
  • Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
  • Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.

Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.

Day 5 — The gap that is not a skill

  • Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
  • Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
  • Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.

Deliverable: Two recordings with a counted reduction in time-to-first-question.

Day 6 — Retest under day-one conditions

  • Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
  • For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
  • Write down which single block you would still lose the offer on.

Deliverable: A second scored rubric placed beside the first, with one named remaining risk.

Day 7 — Full loop under interview conditions

  • Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
  • Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
  • Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.

Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.

Disagreement needs an outcome you did not choose

Conflict answers where you were right and everyone came round are the weakest ones. Stronger: the evidence you went and collected, what would have changed your mind, and what you did in the weeks after the call went against you. Implementing a design you argued against, properly, is a specific and checkable behaviour.

How do you handle feedback or disagreements within a technical team?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Give the blast radius: what could have broken, and what you measured.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

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

What are your professional strengths and weaknesses?

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Name the disagreement and how you resolved it with evidence.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?

Describe a challenging project you worked on and the specific technical hurdles you overcame.

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. Name the disagreement and how you resolved it with evidence.
  3. Close with what you would do differently, concretely.

Follow-up

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