Interview guides · ChenMed
ChenMed Software Engineer Interview Questions & Guide 2026
Concurrency drills for ChenMed software engineer prep: races, lock ordering, and what a check-then-act sequence assumes.
The 60-second brief
The Software Engineer role at ChenMed is a high-impact position centered on building and maintaining the technology that powers a value-based healthcare model. You are not just writing code; you are contributing to software solutions that enable physicians and clinical teams to deliver superior care to underserved populations. Your work directly influences the efficiency of clinical operations and the quality of patient outcomes.
Getting the code to run is the floor. What usually separates answers is the case checked without prompting: empty input, a single element, duplicate keys, or a value that overflows the integer type you chose.
Focus areas
- Authorise every identified read against current consent
- Model bitemporal coverage and retroactive eligibility changes
- Version clinical results instead of updating rows in place
Overview
This role requires a blend of technical precision and a passion for mission-driven work. You will navigate complex problem spaces, often working within the intersection of healthcare data, clinical workflows, and scalable enterprise architecture. Successful engineers here are those who thrive in collaborative, fast-paced environments where the ability to translate abstract clinical needs into robust, maintainable software is paramount.
While the environment can be fast-paced and demanding, candidates who align with ChenMed’s mission to transform healthcare often find the work deeply rewarding and intellectually stimulating.
The ChenMed interview process
ChenMed candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screening
The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.
What to demonstrate
- Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
- Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
- Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year
How to prepare
- Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
- Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
- Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter
Technical Assessment
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
Architectural Discussions
When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.
What to demonstrate
- Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
- Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
- Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
- Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened
How to prepare
- Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
- Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
- Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure
Hands-on Coding Tasks
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
Where candidates lose points
Treating a medical record number or a member ID as a globally unique key and joining on it directly.
These identifiers are unique only within the authority that issued them. Two facilities in one network routinely have the same medical record number for different people, and member IDs get reissued when someone changes plans. Joining on the bare value merges two patients' records, which is the most damaging failure available in this domain, and it passes every test written against a single-facility fixture because the collision only appears once a second source is connected.
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.
Writing code before the input contract is pinned down
Before the first line, state the types, the size bounds, whether duplicates, negatives or an empty input are possible, whether the input is sorted, whether you may mutate it, and what the function returns when nothing matches. Every one of those answers changes the code, and discovering one at minute twenty costs a rewrite you no longer have time for.
Not asking what the system looks like if it dies halfway through
For any multi-step write, say what state remains if the process stops between step two and step three, and what brings it back: a single transaction, a saga with compensating actions, an outbox, or a reconciliation job. Partial failure is routine at any real call volume, so 'that shouldn't happen' is an answer with nothing behind it.
Questions & practice
Resolve enterprise identity from a link log that supports unmerge
Hard · union-find · rollback · identity resolution
person_identity_link holds link_id, enterprise_person_id, assigning_authority, source_person_id, match_score, link_status (auto_linked, manual_linked, potential_duplicate, unlinked, rejected), version, superseded_by_link_id, decided_by, decided_at. You have up to 40 million source identities and 60 million decisions applied in decided_at order. Build a structure answering which enterprise identity a given (assigning_authority, source_person_id) resolves to after any prefix of the log, and supporting a revert of the k most recent merges at O(1) each. State the query complexity, and say why path compression is unavailable to you.
Approach
- Intern the node key as the pair (assigning_authority, source_person_id) into a dense integer index. Never key on source_person_id alone: two facilities in one network routinely issue the same medical record number to different people, so a bare-value key merges two patients before any matching logic has run, and a single-source test fixture will never show it.
- Classify the log rows before touching the structure. auto_linked and manual_linked are unions; potential_duplicate and rejected are recorded decisions that must not union anything; unlinked is a revert of the link it supersedes, not a new edge.
- Use union by size with an explicit undo stack, pushing (attached_root, its previous parent, the previous size of the absorbing root) on every union. Find walks parent pointers to the root: union by size bounds tree height at log2(n), so a query is O(log n) worst case, a union is O(log n), and a revert pops two words and is O(1).
- Path compression is the thing you have to give up, and the reason is specific: it rewrites parent pointers of nodes that were never named in the union being recorded, so the undo entry no longer describes the mutation that happened and a revert restores a forest that is quietly wrong. Near-constant find is only available to an append-only structure.
- Hold enterprise_person_id as a property of the root slot rather than a value copied onto every member. A merge then relabels one slot instead of n rows, and an unmerge restores two labels instead of reconstructing n.
- State the price plainly: every read path gains a resolution hop and no downstream table can carry a plain patient_id column. That cost is what buys reversibility, and it belongs in the design discussion rather than being discovered later by whoever writes the first join.
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?
Reconstruct what the chart displayed at five million past instants
Hard · external sort · as-of join · io bound
observation_result holds 200 million versions: observation_id, enterprise_person_id, filler_order_id, loinc_code, value_numeric, result_status, collected_ts, issued_ts, version, supersedes_observation_id. An incident review hands you 5 million queries of (enterprise_person_id, filler_order_id, loinc_code, as_of_instant). For each, return the version that was visible at that instant, meaning the one with the greatest issued_ts at or before it. Neither side fits in memory. The per-query scan is correct. Explain precisely why it is too slow, then give a plan with its complexity.
Approach
- Name the clock before naming an algorithm. Visibility is issued_ts, the release time. collected_ts is when the specimen was drawn and can precede release by hours, so ordering on it reports a correction as visible long before anyone could have seen it. No amount of index work rescues the wrong column.
- Be exact about why the naive plan fails, because the interviewer is testing whether you can tell arithmetic cost from I/O cost. Per query the chain scan is O(V_k) and the arithmetic is trivial, but 5 million independent lookups into a 200-million-row structure that does not fit in RAM is 5 million random reads. The job is bounded by seeks per query, not by comparisons, and buying a faster comparison changes nothing.
- Convert random access into sequential access. Hash-partition both sides on the chain key (enterprise_person_id, filler_order_id, loinc_code) into P shards sized to fit memory, sort each shard's versions by (chain key, issued_ts) and its queries by (chain key, as_of_instant), and sweep the pair in lockstep. Total O((V + Q) log(V + Q)) with external sort, replacing Q seeks with two sequential passes.
- Inside a chain the sweep is linear, not logarithmic, because queries are visited in ascending as_of order and the version pointer only moves forward: O(V_k + Q_k) per chain. Binary search per query is the better shape only when Q is small relative to V and the versions are already indexed and resident.
- Return the empty answer as a distinct outcome. A query whose as_of precedes the first issued_ts means nothing was displayed, which is not the same as the earliest value, and is frequently the exact fact the review is chasing.
- Return a version later marked entered_in_error if it was live at the instant asked about. Reconstructing the past means reporting what was on the screen, including what was wrong, and quietly substituting today's truth defeats the purpose of the exercise.
Follow-up
- Read replicas lagged 40 seconds at the time. Does your answer describe what the clinician actually saw, and how would you bound the difference?
- Serve the same question online for a single chart at a p99 under 50ms. What changes?
- One partner changed its filler_order_id format mid-year, so the chain key is not stable. How does that appear in your output, and how do you detect it rather than returning empty answers?
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Worked solution (15 min)
- Take the separator characters from bytes 3 through 7 of the buffer and store them.
- Slice to the first segment terminator and split on the field separator into tokens.
- Read tokens at indices 2, 3, 6 and 9 for MSH-3, MSH-4, MSH-7 and MSH-10.
- Unescape each extracted value with the escape character you read in step one, then convert MSH-7 to UTC.
- Return the tuple, and the hash of it if the ledger stores a fixed-width key.
Expected result: For a message whose MSH reads MSH, then the encoding characters, then LABAPP, SITE-A, EHR, SITE-B, 20260315142233-0400, an empty field, ORU^R01, MSG00042, P, 2.5.1, the key is (SITE-A, LABAPP, MSG00042, 2026-03-15T18:22:33Z). The local time 14:22:33 at offset -0400 is 18:22:33 UTC.
Verify it yourself
- Reading MSH-9 with the same rule lands on ORU^R01. If it lands on the empty field or on P, your index is off by one in one direction or the other.
- Re-encode the same message with a different field separator character and the parser returns the identical key.
- A value containing the escaped field separator decodes to that literal character and does not cause the field to re-split.
- Parsing the message twice yields byte-identical keys, and the same control ID from a different facility yields a different key.
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?
Diagnose why an eligibility lookup ignores its index
Medium · indexing · query planning · partial indexes
A 40-million-row coverage_span serves eligibility lookups with: SELECT coverage_id, plan_id, coverage_order FROM coverage_span WHERE enterprise_person_id = $1 AND valid_to = 'infinity' AND effective_date <= $2 AND (termination_date IS NULL OR termination_date >= $2). An index exists on (effective_date). EXPLAIN (ANALYZE, BUFFERS) shows a sequential scan reading 1.8 million buffers at 2.4 seconds. Explain why that index cannot help, propose the index plus any schema change that makes the predicate index-friendly, and state what you expect the new plan and buffer count to be.
Approach
- Read the predicate for selectivity before touching the index. effective_date <= $2 matches most of a 40-million-row history, so a leading range scan on that column returns a large fraction of the table and the planner correctly prefers a sequential scan over millions of random heap fetches. The index is not being ignored; it is being rejected on cost.
- Put the equality column first. enterprise_person_id = $1 selects a handful of rows out of 40 million, so it must lead the composite index; a b-tree can only use columns after the first range predicate as filters, not as search bounds.
- Turn the open-ended termination into something indexable. The OR ... IS NULL branch forces either a BitmapOr or a post-index filter. Add coverage_end date GENERATED ALWAYS AS (COALESCE(termination_date, DATE '9999-12-31')) STORED and rewrite the predicate to coverage_end >= $2; both COALESCE over a column and a date literal are immutable, so the generated column is legal.
- Make the index partial on WHERE valid_to = 'infinity'. Most rows in a bitemporal table are superseded beliefs, so the partial index is a fraction of the full one, stays in cache, and encodes the predicate for free rather than re-checking it per row.
- Consider the range alternative honestly: a daterange(effective_date, termination_date + 1, '[)') column with a GiST index and the && operator handles overlap queries and supports an EXCLUDE constraint against overlapping active coverage, but GiST lookups are slower than b-tree for this point-in-time pattern. Pick b-tree for the read path and keep GiST only if you also need the overlap constraint.
- Verify rather than assert: re-run EXPLAIN (ANALYZE, BUFFERS) and read Rows Removed by Filter and the shared hit/read split, not just the total time.
Follow-up
- After the change the plan is still a sequential scan for one particular member. What single query tells you whether that is stale statistics, a genuinely huge row count for that person, or parameter-dependent plan caching?
- Your generated column needs backfilling on a live table. Does adding a STORED generated column rewrite it, and what does that mean for your maintenance window?
- How would you serve the same lookup when the answer must be as of a past system-time instant rather than the current belief, given your index is partial on valid_to = 'infinity'?
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Worked solution (35 min)
- Open two psql sessions against a scratch database and seed one accumulator at deductible_applied_cents = 40000, plan_deductible_cents = 100000.
- Run the interleaving by hand with both writes absolute, and confirm the row ends at 70000 while the two lines between them charged the member 55000 of deductible — the serial answer is 95000, so 25000 of applied deductible has been lost.
- Run the control: repeat the identical interleaving with both UPDATEs written as SET deductible_applied_cents = deductible_applied_cents + :amt, and confirm the row ends at 95000. That form does not lose the update at READ COMMITTED, which is how you know the defect is the read-modify-write in application code rather than the isolation level by itself.
- Re-run the absolute version with FOR UPDATE on both SELECTs and confirm session two blocks, re-reads 65000, and writes 95000.
- Re-run both sessions at REPEATABLE READ and capture the exact error text and SQLSTATE from the losing session.
- Write the retry wrapper and show what makes the retry safe: a uniqueness key on (accumulator_key, claim_line_id) in an application ledger, so a re-run of an already-applied line is a no-op rather than a second application.
- Write the one-statement LEAST form with RETURNING and say where the member's share now comes from, given RETURNING carries no pre-image before PostgreSQL 18.
Expected result: Seed: deductible_applied_cents = 40000, plan_deductible_cents = 100000. Interleaving: T1 BEGIN; SELECT deductible_applied_cents -> 40000; application computes line A's deductible share as 25000 and the new total as 65000. T2 BEGIN; SELECT -> 40000, because T1 has not committed and READ COMMITTED shows only committed rows; application computes line B's share as 30000 and the new total as 70000. T1 UPDATE accumulator SET deductible_applied_cents = 65000 WHERE key...; COMMIT. T2 UPDATE accumulator SET deductible_applied_cents = 70000 WHERE key...; it blocks on T1's row lock, and on unblocking writes its absolute 70000 over the committed 65000; COMMIT. The row reads 70000 while the two lines charged the member 55000, so the serial answer is 95000 and T1's 25000 is lost; the accumulator understates by 25000 and the member is billed deductible again after the 100000 is already met. The write must be absolute for this to happen — SET deductible_applied_cents = deductible_applied_cents + :amt blocks, re-reads the committed row and applies both increments. Fixes: (1) SELECT ... FOR UPDATE on the read, no SQLSTATE to handle, serialises per member for the transaction's whole duration and therefore forbids external calls inside it; (2) REPEATABLE READ or SERIALIZABLE plus retry on SQLSTATE 40001 ('could not serialize access due to concurrent update' under REPEATABLE READ; SERIALIZABLE raises the same SQLSTATE for read/write dependencies), which needs an idempotency key because the whole transaction re-runs; (3) hash-partition claims by enterprise_person_id to one writer per partition, zero contention at the cost of head-of-line blocking and rebalancing.
Verify it yourself
- The unfixed script reproduces 70000 on a real database rather than on paper, and replaying the same two lines serially reproduces 95000.
- The control run with in-SQL relative increments, under the identical interleaving, ends at 95000 — that isolates the defect to the application-side read-modify-write and not to READ COMMITTED itself.
- With FOR UPDATE, session two's SELECT visibly blocks until session one commits, and the final value is exactly 95000.
- At REPEATABLE READ the losing session reports SQLSTATE 40001 with 'could not serialize access due to concurrent update', and its transaction is left in an aborted state requiring ROLLBACK.
- Applying the same claim_line_id twice through the retry path leaves the accumulator unchanged on the second attempt.
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?
How would you explain the concept of an object to a peer?
Medium · system design and architecture
Approach
- Choose a partition key and say what query it makes expensive.
- Name the failure you are designing for, then the recovery path.
- Fix the scope first: who calls this, how often, and what they do when it fails.
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?
Explain the SOLID principles and provide concrete examples of how you have applied them in your code.
Medium · system design and architecture
Approach
- State the consistency you need, and where you are willing to be stale.
- Name the failure you are designing for, then the recovery path.
- Name the read and write paths separately; they rarely have the same bottleneck.
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?
Describe your knowledge of JBOSS configuration.
Medium · system design and architecture
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- State the consistency you need, and where you are willing to be stale.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
Eligibility reads with an as-of date and a payer timeout
Easy · caching · invalidation · degraded-mode · timeout-budget
The eligibility service answers whether a person is covered for a service date and what they will owe. Peak is roughly 4,000 requests per second between 07:00 and 10:00 across several time zones. Locally held coverage_span rows (enterprise_person_id, payer_id, plan_id, effective_date, termination_date, status, valid_from, valid_to) answer most calls, but a real-time inquiry to the external payer takes one to eight seconds and fails outright a few times an hour. Specify the cache key, the time-to-live, the timeout budget, and exactly what registration receives when the payer is unreachable.
Approach
- Key the answer on (enterprise_person_id, payer_id, plan_id, service_date) and carry the as_of instant inside the value. Coverage terminates retroactively, so the same tuple can be covered and not covered for the same service date, differing only in when the answer was computed; a key without service_date lets today's answer serve tomorrow's question.
- Set the time-to-live from the exposure you are willing to own rather than from the hit rate. Enrolment files land daily to weekly, so a retroactive termination becomes knowable within a day; a 15-minute entry bounds the window in which you keep serving an answer you have already been told is wrong, and costs roughly one origin call per member per morning. Pair it with an explicit invalidation driven by the enrolment loader publishing changed person identifiers.
- Budget the external round trip as a chain that fits inside the caller's deadline: compute the local answer first, then a 2-second cap on the payer inquiry inside a 2.5-second caller deadline. The external call is an upgrade to the local answer, not the origin of it.
- Decide the degraded behaviour explicitly and label it in the response: return the locally derived answer with its source and as_of set, never a fabricated 'covered'. Registration proceeds with an unverified flag and the encounter is queued for re-verification. Blocking registration on a partner's outage converts someone else's incident into a care-delivery incident.
- Name the trade-off rather than implying there is none: you are choosing availability and accepting bounded staleness, and the cost lands weeks later as retroactive denials. Quantify it as unverified encounters per outage minute so it is a decision someone signed rather than a default nobody chose.
Worked solution (20 min)
- Write the cache key, the value struct including as_of and source, and the time-to-live, then state in minutes the maximum staleness that combination permits.
- Draw the timeline for a file received on the fifth terminating coverage effective the first, and mark every interval in which a stale covered answer can still be served.
- Write the timeout budget as explicit numbers that sum to less than the caller's deadline.
- Specify the degraded response shape and name the downstream consumer that acts on the unverified flag.
Expected result: A cache key of (enterprise_person_id, payer_id, plan_id, service_date) whose value carries an as_of instant and a source, a time-to-live justified as an exposure bound plus an enrolment-driven invalidation, a payer timeout that fits inside the caller deadline, and a degraded answer that is labelled unverified and queued for re-verification instead of blocking registration.
Verify it yourself
- Two requests for the same person and plan but different service dates resolve to different cache entries.
- Terminating a coverage effective 20 days in the past changes the served answer for a service date inside that window within one time-to-live.
- With the payer endpoint black-holed, the p99 of the eligibility call stays under the caller deadline and no request returns a hard error.
- Every cached answer can state the instant it was computed as of.
Follow-up
- The payer responds 'active' while your local coverage_span says terminated effective three days ago. Which answer wins, and what do you store?
- A cache flush at 07:15 sends every registration desk to the origin for the same plan at once. How do you hold it to one origin call per key?
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
- 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.
- 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.
- 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.
- 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.
- 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 a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.
Day 1 — Numbers before diagrams
- Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
- Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
- Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.
Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.
Day 2 — One system, from requirements to schema
- Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
- Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
- Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.
Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.
Day 3 — The consistency you are actually buying
- Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
- Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
- Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.
Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.
Day 4 — Failure is the design
- For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
- Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
- Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
- Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.
Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.
Day 5 — Scaling the hot path
- Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
- Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
- Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
- Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.
Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.
Day 6 — Keep the coding hand in, at the bar that applies to you
- Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
- Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
- Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.
Deliverable: One problem hardened to review standard, with its test list and one written review comment.
Day 7 — Defend it while being interrupted
- Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
- Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
- Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.
Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.
Review and mentoring: what you let through
Counting review comments or mentees proves nothing. The useful version is a specific change you approved with a reservation you stated, or one you blocked and the delay that cost. Say which standard you were holding and why it was worth the friction. A mentoring story needs the thing the other person can now do without you.
How do you contribute to an agile development process?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- 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?
How do you handle disagreements regarding technical implementation or architectural direction?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
What is your experience with Java Spring Boot and adding specific annotations for project configuration?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?