cswork / libraryOpen practice workspace

Interview guides · Alpaca Health

Alpaca Health Software Engineer Interview Questions & Guide 2026

Alpaca Health software engineer prep on partial failure: a timed-out request has an unknown outcome, not a failed one.

The 60-second brief

As a Software Engineer at Alpaca Health, you are at the intersection of complex engineering and mission-driven healthcare technology. Your work directly impacts how patients interact with medical services, requiring you to build robust, scalable, and intuitive platforms that prioritize data integrity and user experience. Whether you are working on the frontend to improve patient accessibility or the backend to streamline clinical workflows, your contributions support Alpaca Health's ability to deliver care efficiently.

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

Focus areas

  • Make HL7 and X12 ingestion idempotent under replay
  • Version clinical results instead of updating rows in place
  • Authorise every identified read against current consent

Overview

This role is both technically demanding and strategically significant. You will often be tasked with translating ambiguous clinical requirements into clean, maintainable code, requiring a high degree of collaboration with cross-functional teams. You will have the opportunity to influence product architecture and solve problems that have a tangible, positive effect on real-world health outcomes. Candidates report that Alpaca Health favors engineers who think holistically about the system, making sure every shipped feature is as reliable as it is innovative.

The Alpaca Health interview process

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

Technical Screening

Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.

What to demonstrate

  • Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
  • Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
  • Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
  • Whether a failing case is isolated and explained before any edit is made to the code

How to prepare

  • From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
  • Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
  • Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.

Deep-Dive 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.

Where candidates lose points

Storing monetary amounts as floating-point numbers.

Binary floating point cannot represent values like 0.10 exactly, so summing allowed and paid amounts across millions of claim lines accumulates representation and rounding error. Reconciliation against a remittance file is exact to the cent, so the drift surfaces as a balance that is off by a few cents with no identifiable cause, and engineers lose days looking for a logic bug in code that is correct apart from its numeric type. Integer minor units or an exact decimal type removes the class entirely.

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.

Naming no test cases at all

State what you would test before being asked: empty input, a single element, all elements equal, the maximum permitted size, and the input that exercises the branch you just wrote. It costs thirty seconds and is much of what separates someone who has shipped code from someone who has only solved puzzles.

Choosing a schema before the access patterns are known

Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.

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?

Resolve enterprise identity from a link log that supports unmerge

Hard · union-find · rollback · identity resolution

person_identity_link holds link_id, enterprise_person_id, assigning_authority, source_person_id, match_score, link_status (auto_linked, manual_linked, potential_duplicate, unlinked, rejected), version, superseded_by_link_id, decided_by, decided_at. You have up to 40 million source identities and 60 million decisions applied in decided_at order. Build a structure answering which enterprise identity a given (assigning_authority, source_person_id) resolves to after any prefix of the log, and supporting a revert of the k most recent merges at O(1) each. State the query complexity, and say why path compression is unavailable to you.

Approach

  1. Intern the node key as the pair (assigning_authority, source_person_id) into a dense integer index. Never key on source_person_id alone: two facilities in one network routinely issue the same medical record number to different people, so a bare-value key merges two patients before any matching logic has run, and a single-source test fixture will never show it.
  2. Classify the log rows before touching the structure. auto_linked and manual_linked are unions; potential_duplicate and rejected are recorded decisions that must not union anything; unlinked is a revert of the link it supersedes, not a new edge.
  3. Use union by size with an explicit undo stack, pushing (attached_root, its previous parent, the previous size of the absorbing root) on every union. Find walks parent pointers to the root: union by size bounds tree height at log2(n), so a query is O(log n) worst case, a union is O(log n), and a revert pops two words and is O(1).
  4. Path compression is the thing you have to give up, and the reason is specific: it rewrites parent pointers of nodes that were never named in the union being recorded, so the undo entry no longer describes the mutation that happened and a revert restores a forest that is quietly wrong. Near-constant find is only available to an append-only structure.
  5. Hold enterprise_person_id as a property of the root slot rather than a value copied onto every member. A merge then relabels one slot instead of n rows, and an unmerge restores two labels instead of reconstructing n.
  6. State the price plainly: every read path gains a resolution hop and no downstream table can carry a plain patient_id column. That cost is what buys reversibility, and it belongs in the design discussion rather than being discovered later by whoever writes the first join.

Worked solution (40 min)

  1. Intern three nodes: A is (AUTH1, MRN-77), B is (AUTH2, MRN-77), C is (AUTH1, MRN-90).
  2. Apply union(A, C) from a manual_linked row, pushing its undo entry.
  3. Apply union(A, B) from an auto_linked row with a high match score, pushing its undo entry.
  4. Query all three, then pop one undo entry and query all three again.
  5. Assert the parent and size arrays element-wise against a snapshot taken before the second union.

Expected result: After the first union, A and C share a root and B is its own root. After the second, all three share a root. After reverting one union, B is its own root again and A and C remain merged, with the first union's record untouched. Note what the key bought you: A and B collide on MRN-77 and would have been one identity from the first insert under a bare-value key, with no decision row ever recorded.

Verify it yourself

  • After reverting all k unions, the parent and size arrays are identical element by element to the pre-union snapshot, not merely equal in their query answers.
  • Tree height never exceeds log2(n). Assert it, because one union that attaches by arbitrary root instead of by size breaks the find bound without any visible symptom.
  • Applying a potential_duplicate or rejected row changes no query answer and pushes nothing onto the undo stack.
  • Querying (AUTH1, MRN-77) and (AUTH2, MRN-77) before any link row returns two distinct identities.

Follow-up

  • A merge is found wrong three weeks and 200,000 unions later, and your stack only reverts a suffix. What do you do instead, and what does that cost?
  • A claim was posted under the losing identity before the merge. After the unmerge, which identity owns it, and what in your design let you answer that?
  • Two matcher instances submit unions concurrently. What is the smallest change that keeps both the structure and the undo stack consistent?

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.

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?

Add a backfilled NOT NULL column to a live encounter table

Hard · online migration · ddl locks · backfill

encounter holds 900 million rows and is written continuously by the admit/discharge/transfer consumer. You must add admission_source_code text, backfill it per row from a lookup on the source message archive, constrain it to a fixed code set, make it NOT NULL, and add an index on (facility_id, admit_ts) — with no write outage and no statement blocked longer than one second. Give the ordered steps, the lock level each takes, why the obvious single ALTER is unavailable here, and the session setting that stops a DDL statement from stalling everything behind it.

Approach

  1. Say why the shortcut does not apply. Since PostgreSQL 11, ADD COLUMN with a constant default is metadata-only and cheap, but this value is derived per row from an archive lookup, so there is no constant to store and the table must be written row by row regardless.
  2. Add the column nullable first: catalog-only, ACCESS EXCLUSIVE for microseconds. The danger is not the statement's duration but the lock queue — a pending ACCESS EXCLUSIVE blocks every reader that arrives behind it, so a long-running report turns a microsecond DDL into a multi-minute outage. SET lock_timeout = '1s' before the ALTER and retry on failure; that converts a potential outage into a retried statement.
  3. Deploy dual-write before backfilling. The consumer must populate the column on every insert and transfer from that moment, or the backfill chases a moving tail forever.
  4. Backfill in bounded batches over primary-key ranges, a few thousand rows per transaction with a pause between, restricted to WHERE admission_source_code IS NULL so it is resumable and idempotent. One giant UPDATE holds a transaction open for hours, pins the xmin horizon so autovacuum cannot clean anything, and bloats the table by a full row version per updated row.
  5. Add the value constraint as CHECK (...) NOT VALID first — ACCESS EXCLUSIVE, no scan — then ALTER TABLE ... VALIDATE CONSTRAINT, which takes only SHARE UPDATE EXCLUSIVE and scans without blocking reads or writes.
  6. Reach NOT NULL without a blocking scan: add CHECK (admission_source_code IS NOT NULL) NOT VALID, VALIDATE it, then SET NOT NULL, which on PostgreSQL 12 and later uses the validated constraint as proof and skips the full scan; drop the now-redundant CHECK afterwards. Build the index with CREATE INDEX CONCURRENTLY, which takes SHARE UPDATE EXCLUSIVE, makes two passes plus a wait for concurrent transactions, cannot run inside a transaction block, and leaves an INVALID index behind on failure that must be dropped and rebuilt.

Follow-up

  • CREATE INDEX CONCURRENTLY has been running for four hours and you need to cancel it. What state is the index left in, how do you detect it, and what do you run next?
  • Your backfill is at 40% and the replica is 90 seconds behind. What do you change, and what do you measure to decide the new batch size?
  • The code set gains a value two weeks later. Does your CHECK constraint or a lookup table with a foreign key make that change cheaper, and what does each cost on the write path?

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?

Explain how you would handle asynchronous data processing for clinical reporting.

Medium · system design and architecture

Approach

  1. Name the failure you are designing for, then the recovery path.
  2. Name the read and write paths separately; they rarely have the same bottleneck.
  3. Choose a partition key and say what query it makes expensive.

Follow-up

  • What would you drop to keep the system up under load?
  • What breaks first when traffic grows ten times?

How do you ensure data security and HIPAA compliance when building new user-facing features?

Medium · system design and architecture

Approach

  1. Fix the scope first: who calls this, how often, and what they do when it fails.
  2. Name the failure you are designing for, then the recovery path.
  3. State the consistency you need, and where you are willing to be stale.

Follow-up

  • What would you drop to keep the system up under load?
  • How does this behave when that dependency is down for an hour?

How would you design a scalable system for managing real-time patient appointments?

Medium · system design and architecture

Approach

  1. State the consistency you need, and where you are willing to be stale.
  2. Fix the scope first: who calls this, how often, and what they do when it fails.
  3. Choose a partition key and say what query it makes expensive.

Follow-up

  • How does this behave when that dependency is down for an hour?
  • What would you drop to keep the system up under load?

Evolve a coverage contract to bitemporal without breaking shipped clients

Hard · versioning · backward compatibility · bitemporal

GET /coverage/{person_id} has for years returned a flat list of {payer_id, plan_id, effective_date, termination_date, status}, where a null termination_date means open-ended. Callers now need to ask coverage questions as of a past service date, and to see what the system believed at an earlier instant, because retroactive terminations are routine traffic. Dozens of deployed clients cannot be upgraded on our schedule and some reject unknown fields. Design the evolution: the new shape, the versioning mechanism, the default behaviour for an old client, and the deprecation path with the checkpoints that gate it.

Approach

  1. Classify the change before choosing a mechanism. Adding a field is safe only under two preconditions: no client validates against a closed schema, and no client reads the object and writes it back, because a round-trip silently drops fields the client does not model. The prompt states some clients reject unknown fields, so 'just add as_of' is not additive here - that is the whole difficulty.
  2. Separate the two time axes the new shape must expose, since conflating them is the usual bug. Business time answers which coverage period applies to a service date; system time answers what we believed at an instant. The new resource takes both - an as_of date and an asserted_at instant - and the old response is exactly the projection as_of = today, asserted_at = now.
  3. Choose the version carrier and defend it. A new path is blunt but unmistakable and caches cleanly; a media-type parameter negotiates per request but is ignored by clients that do not set Accept and by any cache not keyed with Vary; a query parameter is trivial to set and pollutes cache keys. A new resource path for the shape change plus strictly additive evolution within each version is the defensible combination, and under any choice the old URL must keep returning the old bytes, null-means-open-ended included.
  4. Never widen an existing field's meaning. If 'termination unknown' now needs representing, it cannot be null, because every deployed client already reads null as open-ended; it needs a new, explicitly enumerated field present only in the new shape. Redefining a value in place is the change that alters every client's behaviour without a single one being redeployed.
  5. Run the deprecation on evidence rather than a calendar. Instrument per-client usage of the old resource, serve Deprecation and Sunset headers with a Link to the successor, and gate each checkpoint - announce, stop new clients, contact the remaining callers by name, freeze, remove - on that metric. State what an unmigrated client gets at sunset: an explicit, documented error, never a silently different shape.

Worked solution (40 min)

  1. Write the old response and the new response side by side and mark every field that was added, removed, or had its meaning changed.
  2. For each marked field write the concrete failure a deployed client suffers - parse error, wrong decision, field dropped on round-trip - and the client behaviour that is its precondition.
  3. Write the projection rule that derives the old shape from the bitemporal store, then test it on a person with a retroactive termination and confirm the old bytes are unchanged.
  4. Choose the version carrier, write request and response for both resources, and state each one's cache key including both time axes.
  5. Write the deprecation timeline with its headers, the per-client usage metric gating each checkpoint, and the response an unmigrated client receives at sunset.

Expected result: The old path is preserved byte-compatibly, serving the projection as_of = today, asserted_at = now. A new resource accepts as_of (business date) and asserted_at (system instant) and returns entries with effective_date, termination_date, an explicitly enumerated termination certainty, coverage_order and the asserted-at window; null termination_date still means open-ended in both shapes. The old path carries Deprecation and Sunset headers with a Link to the successor. Removal is gated on per-client usage reaching zero across announce, freeze-new and direct-contact checkpoints, and an unmigrated client receives a documented 410 at sunset rather than a changed shape.

Verify it yourself

  • A deployed client's bytes are unchanged both for a person with no retroactive corrections and for one with them.
  • No existing field's set of legal values grew.
  • The as-of read's cache key carries both time axes, so two different questions cannot share an entry.
  • Sunset is gated on an observed metric, and an unmigrated client gets an explicit error rather than a different shape.

Follow-up

  • An old client passes a filter parameter you only added in the new version. What do you return?
  • A caching layer sits in front of both resources. What must be in the cache key for an as-of read, and what breaks if it is not?
  • How would you prove no client round-trips the object back to you, before you rely on additive evolution?

One facility stops updating while aggregate ingestion lag looks fine

Medium · poison message · head-of-line blocking · dead-letter queue

Encounters from one sending facility stopped updating at 02:10. Aggregate HL7 ingestion throughput looks normal and the consumer group's total lag is up only 4%. One consumer instance is pinned at 100% CPU. The error log shows the same message control ID repeating about 200 times a second with a parse error on a timestamp field. The stream is partitioned by (sending facility, encounter) to preserve per-encounter ordering. Give the ordered diagnosis, the fix, and what you must not do to the message.

Approach

  1. Look at lag per partition, never the aggregate. With 64 partitions, one fully stuck partition raises total lag by about 1.6% while that facility's encounters stop entirely, so the aggregate metric is structurally blind to head-of-line blocking. The alert that would have caught this is per-partition lag or a committed offset that has not advanced.
  2. Confirm the loop by grouping errors by offset or control ID. The same identifier repeating at the retry rate, with the partition's committed offset frozen, is a poison message. A growing set of distinct identifiers instead means a systematic parse regression, which points at a dialect change on the partner's side and has a different fix.
  3. Classify before acting: parse-time or apply-time failure. A parse failure has written nothing and can be parked safely. An apply failure may have committed part of its work, so you must confirm the write path is idempotent against a unique constraint on the idempotency key before you replay or skip anything.
  4. Fix the mechanism rather than the one message: bounded retries with exponential backoff and a maximum attempt count, then park the raw bytes, partition key and offset to a dead-letter store and advance the offset. Alert on dead-letter depth, because lag will not catch the next one either.
  5. Handle the ordering consequence explicitly. Silently skipping a message in a per-encounter ordered stream permanently drops a clinical event — a transfer, a discharge, a result release. Mark the encounter key as blocked and reconcile it, rather than letting later messages apply over a gap nobody recorded.
  6. Then repair the parser. Partial-precision HL7v2 timestamps are legal: 2026, 202609, 20260922 and 20260922143000-0700 are all valid, so the parser must carry a precision rather than assume fourteen digits plus an offset, and must reject with a typed error the dead-letter path can classify.

Follow-up

  • What maximum attempt count do you choose, and what does it imply about a transient failure like a downstream store being briefly unavailable, which looks identical to a poison message for the first few attempts?
  • After the parser is fixed, how does an operator replay parked messages for that encounter without re-applying the messages that already succeeded behind it?

Suggested practice checklist

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

Day 1 — Rebuild the primitives by implementing them

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Day 7 — Solve out loud, under time

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

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

Scope is read from what you decided

Team size, service count and tickets closed say very little. Seniority shows in the decision you owned: what you chose not to build, which constraint you traded away, whose objection you had to resolve before anything could move. A large project where you executed someone else's plan is a small story.

How do you handle situations where you are tasked with debugging a critical issue under tight deadlines?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Name the disagreement and how you resolved it with evidence.
  3. 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?

What do you do when you disagree with a peer’s code review feedback?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

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

How do you balance the need for rapid feature delivery with the necessity of technical debt reduction?

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Give the blast radius: what could have broken, and what you measured.
  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?