cswork / libraryOpen practice workspace

Interview guides · Healthcare Realty

Healthcare Realty Software Engineer Interview Questions & Guide 2026

Healthcare Realty software engineer prep on databases: indexes, query plans, and the anomalies an isolation level still permits.

The 60-second brief

At Healthcare Realty, the Software Engineer role—often categorized within the organization as an Application Developer and SDLC Lead—maintains the technical infrastructure that supports the company's real estate portfolio. Beyond writing code, the role builds the systems that let Healthcare Realty's teams manage complex healthcare facilities efficiently. Your work affects how the company tracks maintenance, optimizes operations, and delivers value to its stakeholders in the healthcare sector.

The behavioural round is a technical round in narrative form. Prepare it by collecting specifics you actually owned, such as a design you argued against, an incident you diagnosed, or a decision you later reversed, rather than by rehearsing phrasing.

Focus areas

  • Model bitemporal coverage and retroactive eligibility changes
  • Hold accumulator limits under concurrent claim adjudication
  • Design person merges that remain reversible afterwards

Overview

This role is unique because it blends technical software expertise with a deep understanding of the Software Development Life Cycle (SDLC). You will be responsible for leading development initiatives, ensuring that Healthcare Realty's applications are robust, scalable, and aligned with the operational demands of healthcare real estate. Whether you are improving existing systems or architecting new solutions, your contribution supports the daily functionality of the company's properties and its business operations.

The Healthcare Realty interview process

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

Initial Screening

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

Technical Dives

What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.

What to demonstrate

  • Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
  • Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
  • Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly

How to prepare

  • Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
  • Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
  • Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live

Behavioral Assessments

Your first answer is not really what is scored. It buys the follow-up questions, and those decide the round. An interviewer with fifteen minutes takes one thread and pushes on it four or five times, so a story you can only tell at a single level of detail collapses under the third why. That is an argument for fewer stories known deeply rather than one prepared per prompt. Four or five pieces of work you can still explain down to the code you changed and the argument you had about it will cover nearly anything asked in this round.

What to demonstrate

  • Whether a story holds as the questioning moves from what you did to why that instead of the alternative, and then to what you would change knowing what you know now
  • Whether you can re-cut a project to answer the question actually asked rather than delivering a rehearsed block that answers an adjacent one
  • Whether your level of detail is chosen rather than habitual: going down to the schema when the question is about the data model, staying out of it when the question is about the person who disagreed with you

How to prepare

  • Pick four projects and write the chain out four levels deep for each: what you did, why that, why not the alternative, and what would have to be true for the alternative to have won. Where you cannot reach the fourth level, you have a placeholder rather than a story
  • Have someone ask why three times in a row on a single thread with nothing else added, and mark the point where you start repeating a sentence you already said. That point is where the interviewer stops learning anything
  • Build a one-page index instead of an answer bank: the common prompts in this round (disagreement, a failure that was yours, thin requirements, a deadline you missed, work you inherited) mapped to which of your four projects you would use for each, so the choosing is done now rather than while an interviewer waits

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.

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.

Quoting amortised or average cost as if it were a worst-case guarantee

Appending to a dynamic array is amortised O(1), but the append that triggers a resize copies every element, and hash lookup is constant only while the hash spreads the actual keys. Say which guarantee you are offering when the caller cares about the latency of one call rather than the total over many.

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.

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?

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

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

Worked solution (45 min)

  1. Choose P so a shard fits in memory: at P equal to 256, each shard holds about 781,000 versions and about 19,500 queries.
  2. Hash-partition versions and queries on the chain key, writing both to shard files.
  3. Per shard, sort versions by (chain key, issued_ts) and queries by (chain key, as_of_instant).
  4. Sweep the two sorted streams together, advancing the version pointer while issued_ts is at or before as_of and answering from the last version passed.
  5. Emit an explicit empty answer when the pointer has not advanced past any version for that chain, and concatenate the shard outputs.

Expected result: A chain with version 1 preliminary 5.2 issued 09:12, version 2 final 5.4 issued 11:40, version 3 corrected 7.1 issued 14:02 answers: as_of 08:00 returns nothing displayed, 10:00 returns 5.2 preliminary, 12:00 returns 5.4 final, 14:10 returns 7.1 corrected. If version 3 is marked entered_in_error at 18:00, the 14:10 query still returns 7.1, because that is what was on the screen. The whole job is two sequential passes over 200 million rows instead of 5 million seeks.

Verify it yourself

  • Every answer's issued_ts is at or before its as_of_instant, and the next version in that chain has an issued_ts strictly after it. That pair pins the boundary exactly.
  • A query landing exactly on an issued_ts returns that version, not its predecessor, which is the inclusive boundary the definition states.
  • Shuffling the query set and re-running returns identical answers. If it does not, a pointer is surviving across a chain boundary.
  • Spot-check twenty chains against a direct per-query scan, since an off-by-one in a lockstep sweep is invisible in aggregate.

Follow-up

  • Read replicas lagged 40 seconds at the time. Does your answer describe what the clinician actually saw, and how would you bound the difference?
  • Serve the same question online for a single chart at a p99 under 50ms. What changes?
  • One partner changed its filler_order_id format mid-year, so the chain key is not stable. How does that appear in your output, and how do you detect it rather than returning empty answers?

Resolve enterprise identity from a link log that supports unmerge

Hard · union-find · rollback · identity resolution

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

Approach

  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.

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?

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

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

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?

Namespace source identifiers so a cross-facility join cannot collide

Easy · identity · composite keys · constraints

person_identity_link holds one row per (assigning_authority, source_person_id) per link version: link_id, enterprise_person_id, assigning_authority, source_person_id, match_score, link_status in ('auto_linked','manual_linked','potential_duplicate','unlinked','rejected'), version, superseded_by_link_id, decided_by, decided_at. Two facilities in one network both issued medical record number 004821, to different people. An existing extract joins on source_person_id alone and has been merging their charts. Give the constraint set that makes the bare join impossible to write by accident, then write the query that resolves one facility's (authority, MRN) to its current enterprise_person_id.

Approach

  1. Name the root cause precisely: an MRN is unique only inside the authority that issued it, and a member ID only inside payer plus plan. The identifier is half a key; the namespace is the other half.
  2. Make the composite the only addressable key. UNIQUE (assigning_authority, source_person_id, version) gives version history; a partial unique index on (assigning_authority, source_person_id) WHERE superseded_by_link_id IS NULL enforces one live decision per source record.
  3. Remove the bare column as a join target from every consuming view: expose a view or function that takes both arguments, so a one-argument lookup is a compile-time error rather than a silent cross-patient merge.
  4. Write the resolution query against the live version only, and filter link_status separately — 'unlinked' and 'rejected' are live rows that mean 'no enterprise identity', so they must not be confused with 'no row'.
  5. State the detection query you would run before trusting any existing extract: group source_person_id across authorities and count distinct authorities, which surfaces every collision already in the data.

Worked solution (20 min)

  1. Write the two constraints and say which invariant each one holds: the three-column UNIQUE holds version history, the partial unique index holds single-live-decision.
  2. Write the resolution query filtering on superseded_by_link_id IS NULL plus the two linked statuses.
  3. Write the collision detector over the existing table and run it mentally against the 004821 case to confirm it returns that value with two authorities.
  4. Decide the contract for zero rows versus a live 'unlinked' row: zero rows means the source record was never seen, 'unlinked' means it was seen and deliberately not attached, and the caller must handle them differently.
  5. Write the one-line fix for the broken extract: add the authority column to the extract and to the join, then re-run and diff the row count against the old output.

Expected result: Constraints: UNIQUE (assigning_authority, source_person_id, version) and CREATE UNIQUE INDEX ON person_identity_link (assigning_authority, source_person_id) WHERE superseded_by_link_id IS NULL. Resolution: SELECT l.enterprise_person_id FROM person_identity_link l WHERE l.assigning_authority = :aa AND l.source_person_id = :mrn AND l.superseded_by_link_id IS NULL AND l.link_status IN ('auto_linked','manual_linked'); zero or one row by construction. Detector: SELECT source_person_id, count(DISTINCT assigning_authority) AS authorities, array_agg(DISTINCT assigning_authority) FROM person_identity_link GROUP BY 1 HAVING count(DISTINCT assigning_authority) > 1;

Verify it yourself

  • The detector returns 004821 with two authorities, confirming the collision is in the data and not a theory.
  • Inserting a second live row for one (authority, MRN) raises a unique violation, while inserting a superseded row for the same pair succeeds.
  • The resolution query returns at most one row for every (authority, MRN) pair in the table — run it as a LATERAL over the distinct pairs and assert no count exceeds one.
  • Re-running the repaired extract yields a strictly different enterprise_person_id for at least one of the two 004821 patients.

Follow-up

  • The spine stores enterprise_person_id directly on encounter and claim_line. What does a wrong merge cost you with that design, and what would you store instead to keep unmerge a supported operation?
  • A link is downgraded from auto_linked to potential_duplicate. Should the resolution query return the old enterprise identity, nothing, or an error, and who decides?
  • Your partial unique index blocks a second live row. How does the merge path insert the new version and supersede the old one without violating it mid-transaction?

How do you approach documentation for complex systems to ensure long-term maintainability?

Medium · system design and architecture

Approach

  1. Choose a partition key and say what query it makes expensive.
  2. Name the read and write paths separately; they rarely have the same bottleneck.
  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 do you evaluate whether to build a custom solution or integrate an existing third-party tool?

Medium · system design and architecture

Approach

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

Follow-up

  • How does this behave when that dependency is down for an hour?
  • What breaks first when traffic grows ten times?

Explain how you balance performance requirements with user experience constraints.

Medium · system design and architecture

Approach

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

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?

How do you ensure code quality and consistency across a distributed development team?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  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?

How do you approach the design phase of a new application to ensure scalability?

Medium

Approach

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

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?

Patient summary p99 blows up after version resolution ships

Easy · n+1 query · latency regression · versioned reads

The longitudinal record service serves a patient summary that fans out across eight to twelve resource types. After a release, the composite read's p99 went from 180ms to 2.1s while p50 moved from 40ms to 55ms. Database CPU is flat, no statement appears in the slow-query log, and the regression is worst for patients with long histories. The release added a latest-version resolution step so superseded observation_result rows stop appearing. Find the cause and give the fix, including the index it needs.

Approach

  1. Read the p50/p99/p99.9 split before anything else. p50 nearly flat with p99 up twelve-fold means the added work scales with a per-patient quantity — the number of resource versions on the chart — rather than a uniform slowdown. CPU saturation, lock waits and replica lag all move p50 too, so they are already excluded.
  2. Count database round trips per request, not query time. The signature in pg_stat_statements is one statement whose calls grew by orders of magnitude while its mean_exec_time stayed under a millisecond: a cheap query executed once per row. That is exactly why nothing crossed the slow-query threshold.
  3. Read the resolution code. It loops over the fetched rows and issues one query per row to test whether a newer version supersedes it, so the cost is 1 + R round trips for R versions. At 3ms of round trip each, a 600-version chart costs about 1.8s of pure latency with negligible database work.
  4. Replace it with one set-wise query per resource type: SELECT DISTINCT ON (accession_id, loinc_code) ... WHERE enterprise_person_id = $1 ORDER BY accession_id, loinc_code, version DESC, backed by an index on (enterprise_person_id, accession_id, loinc_code, version DESC). Round trips fall from 1 + R to the number of resource types, at most twelve, and the fan-out can run those concurrently.
  5. Name the trade-off against the alternatives. An anti-join on NOT EXISTS (SELECT 1 FROM observation_result o2 WHERE o2.supersedes_observation_id = o.observation_id) is correct against this schema but plans as a second pass over the person's rows. A maintained superseded marker on the predecessor row indexes best but adds a write to a table whose whole guarantee is that rows are never rewritten; DISTINCT ON keeps the append-only property.

Follow-up

  • A correction commits while the summary is being assembled across eight separate queries. Which version does the caller get, and how do you make the composite read consistent — a repeatable-read snapshot, or an as-of timestamp threaded through every query?
  • Would you cache the resolved latest-version set per patient? Corrections are ordinary traffic, so state the invalidation trigger and the worst-case staleness before answering yes.

Suggested practice checklist

For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.

Day 1 — Measure before reasoning

  • Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
  • Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
  • Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.

Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.

Day 2 — References, copies, and the bugs they produce

  • Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
  • Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
  • Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.

Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.

Day 3 — Types, once, in a language that checks them

  • Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
  • Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
  • Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.

Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.

Day 4 — Concurrency, starting with what actually runs at the same time

  • Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
  • Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
  • Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.

Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.

Day 5 — Debugging as a procedure rather than an instinct

  • Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
  • Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
  • Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.

Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.

Day 6 — Tests that catch the bug you are about to write

  • Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
  • Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
  • Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.

Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.

Day 7 — Debug something broken, out loud

  • Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
  • Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
  • Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.

Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.

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 technical debt while maintaining project velocity?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. State the situation in two sentences and spend the rest on the reasoning.
  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?
  • What would you do differently if you ran that again?

Describe a time you had to pivot your technical approach due to changing requirements.

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?
  • How did you know your change caused the improvement?

Describe your experience leading an SDLC process from requirements gathering to deployment.

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. Close with what you would do differently, concretely.

Follow-up

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