cswork / libraryOpen practice workspace

Interview guides · Baylis Medical

Baylis Medical Software Engineer Interview Questions & Guide 2026

A prep plan for Baylis Medical software engineer candidates: answer first, read the solution second, repeat the misses later.

The 60-second brief

As a Software Engineer at Baylis Medical, you play a vital role in bridging the gap between cutting-edge medical research and the clinical tools that improve patient outcomes. Baylis Medical is a leader in the development and manufacturing of medical devices, particularly in the fields of cardiology, endovascular, and pain management. Your work is not just about writing code; it is about ensuring that the software powering these complex systems is reliable, safe, and intuitive for medical professionals.

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

  • Make HL7 and X12 ingestion idempotent under replay
  • Design person merges that remain reversible afterwards
  • Hold accumulator limits under concurrent claim adjudication

Overview

This position is critical because your contributions directly impact the efficacy and safety of medical devices used in high-stakes clinical environments. You will often work at the intersection of hardware and software, requiring a mindset that values precision, rigorous documentation, and a deep understanding of the end-user's needs. The environment is fast-paced and intellectually demanding, offering you the opportunity to influence the entire product lifecycle, from initial design and prototyping to validation and final deployment.

The work is intellectually rigorous, often requiring you to solve problems that have real-world physical constraints. You should expect to collaborate closely with cross-functional teams, including R&D engineers, quality assurance, and clinical specialists. For a professional who thrives on complexity and is motivated by the tangible impact of their work on healthcare, this role offers a challenging and highly rewarding career path.

The Baylis Medical interview process

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

HR Phone Screen

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

What to demonstrate

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

How to prepare

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

Technical Interviews

Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.

What to demonstrate

  • Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
  • Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
  • Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
  • Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply

How to prepare

  • For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
  • For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
  • Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last

Take-Home Assignment

A deadline measured in days usually covers work measured in hours, and treating the calendar as the effort budget is what produces an over-built submission. Structure out of proportion to the problem reads as poor judgement about cost: an interface with one implementation, a configuration layer for values that never vary, a dependency added for something the standard library already does. The opposite failure, one long function carrying everything, is read the same way. Aim for the least structure that lets you write the tests you want, and record the abstraction you did not build alongside the condition that would justify it.

What to demonstrate

  • Whether each layer, interface and configuration point earns its place against the problem as stated, rather than against a larger imagined version of it
  • Whether every third-party dependency is doing work you would not want to write and maintain, given that each one costs the reviewer an install and a version to trust
  • Whether the work is finished, since a narrower scope completed with tests and a README reads better than a broader one left half-wired
  • Whether the things you chose not to build are recorded, so an omission reads as a decision rather than as something you ran out of time for

How to prepare

  • Write down what done means as a list of behaviours before you open an editor, then stop at that line even when calendar time is left
  • Open a past project of yours, delete every abstraction that has exactly one implementation and one caller, and read the result; that tells you where your own proportion line actually sits
  • Before adding a dependency, write down what it saves you and what the standard library would cost instead, and keep it only when that comparison favours it

Follow-Up Interview

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

Where candidates lose points

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

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

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 the input fits in memory

Ask how large the input is in bytes before committing to an in-memory algorithm; beyond that point the options are a single streaming pass, an external sort with bounded buffers, or a sketch that trades exactness for constant memory. An algorithm that assumes random access to the whole input is a different algorithm from one that sees each element once.

Assuming the bug is in the framework

Suspect your own code first: read the stack trace top to bottom, check which versions are actually installed rather than which ones you believe are, and reproduce in isolation before blaming a library that thousands of people run daily. When the fault really is upstream, you need that minimal reproduction to say so credibly anyway.

Questions & practice

Merge twelve resource streams into one patient summary page

Medium · k-way merge · heap · pagination

A patient summary fans out to between 8 and 12 resource types. Each returns a network-backed, paged iterator of resource versions sorted by issued_ts descending, up to 200,000 versions per type for one person. Return the 50 most recent current versions across all types, where current means no later version supersedes it within the same logical resource, and a logical resource whose current version is entered_in_error is omitted entirely. You may not materialise the iterators. Give time and space in terms of k types, the result size and the page size.

Approach

  1. k-way merge with a max-heap holding one head per iterator, keyed on issued_ts. Seeding is O(k), each pop is O(log k), so reaching R emitted rows costs O(k + P log k) for P pops, with O(k) heap space plus one page buffered per iterator. Fetching everything and sorting is O(V log V) over V up to 2.4 million versions and drags every page across the network to produce 50 rows.
  2. Suppress with a hash set of logical resource ids already seen, recorded on first sight whether or not that version is emitted. A logical resource has exactly one resource type, so all of its versions arrive on one iterator, and that iterator is descending in issued_ts: the first version you see for a logical resource is its newest. Deciding on first sight and suppressing every later pop for that id is therefore correct in one pass with no lookahead.
  3. Count emits, not pops. A correction-heavy chart can burn many pops per emitted row, so a loop that stops at 50 pops returns a short page. Put a bound on total pops as well, and when it trips, return what you have with a continuation token rather than spinning.
  4. Break issued_ts ties deterministically on (resource type, resource id). Without it two identical requests return two different orderings and the next page silently skips or repeats rows.
  5. Handle entered_in_error at first sight: the erroneous version still supersedes its predecessor, so record the id in the seen set and emit nothing, dropping the whole logical resource instead of falling back to the value it replaced. Recording it is the load-bearing half. Skip it and the next pop re-displays the value a clinician already retracted.
  6. Summarise the budget honestly: the composite p99 is what the user feels, and it is bounded below by the slowest of the k iterators, so the merge fixes the ordering cost but not the fan-out tail.

Worked solution (25 min)

  1. Seed the heap with the head of each iterator and an empty emitted-id set.
  2. Pop the maximum by issued_ts, push that iterator's next head, and check the logical resource id against the set.
  3. On first sight record the id, then emit unless that version is entered_in_error; on a repeat sight suppress. Stop when emits reach the target.
  4. Return the rows plus a continuation token carrying the last (issued_ts, type, id) tie-break tuple.

Expected result: Three streams: labs hold A2 at 10:05 which supersedes A1 at 09:00, plus B1 at 08:30; medications hold C1 at 09:30; encounters hold D1 at 10:01. Asking for three rows emits A2, D1, C1 in three pops. Asking for four takes five pops: the fourth pop is A1, suppressed because logical resource A was already seen, and the fifth is B1, emitted. Now mark A2 entered_in_error: asking for three rows takes those same five pops but emits D1, C1 and B1, because the A2 pop records logical resource A and emits nothing, and the A1 pop is then suppressed rather than resurrecting the 09:00 value.

Verify it yourself

  • Emitted rows are non-increasing in issued_ts.
  • No logical resource id appears more than once in the page.
  • Every pop falls into exactly one of three buckets, so pops equal emits plus later versions suppressed plus logical resources dropped as entered_in_error. Pops equal emits only when the popped prefix holds no second version of any logical resource and no first-sight version that is entered_in_error.
  • Running with k set to 1 returns that stream's first 50 surviving current versions in exactly 50 pops plus one per later version suppressed plus one per logical resource dropped as entered_in_error.

Follow-up

  • One of the twelve iterators has a p99 of 400ms while the rest return in 20ms. What is your composite p99 and what would you change first?
  • The user pages to rows 51 through 100. How do you resume without re-reading from the top, and what breaks if issued_ts is not unique?
  • One resource type is accidentally returning ascending order. How would your code detect that rather than quietly emitting the oldest rows?

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.

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?

Flag a requester reading too many distinct charts per window

Medium · sliding window · two pointers · distinct count

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

Approach

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

Follow-up

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

Bitemporal coverage table with retroactive termination queries

Easy · bitemporal · schema design · retroactive correction

You maintain coverage_span: coverage_id, enterprise_person_id, payer_id, plan_id, effective_date, termination_date (nullable, meaning open-ended, not unknown), status, valid_from timestamptz, valid_to timestamptz DEFAULT 'infinity'. An enrolment file received on 2026-03-05 terminates a member effective 2026-03-01, after a claim for service date 2026-03-03 was already adjudicated against the answer then in force. State your termination_date convention, write the two SELECTs — (a) is the person covered on 2026-03-03 as known now, (b) what the system believed on 2026-03-04 — and say precisely what the loader writes when the file lands.

Approach

  1. Separate the two time axes explicitly before writing anything: effective_date/termination_date are business time (when benefit applied), valid_from/valid_to are system time (when this row was the system's belief). Every query names both, or it is ambiguous.
  2. Pin the inclusivity convention, because the predicate depends on it. Enrolment files conventionally carry termination_date as the last covered day, so the business-time predicate is effective_date <= :svc AND (termination_date IS NULL OR termination_date >= :svc). If your feed uses an exclusive end, it becomes > :svc, and a half-open daterange column removes the ambiguity permanently.
  3. Write the as-known-now query by pinning system time to the open version (valid_to = 'infinity') and letting business time float to the service date.
  4. Write the as-of-belief query by pinning system time to an instant: valid_from <= :asof AND valid_to > :asof. Half-open on both ends is what makes 'exactly one row per key per instant' provable.
  5. Describe the write as one guarded statement rather than two independent ones: a data-modifying CTE whose UPDATE stamps the open row's valid_to to now() (the only UPDATE the design permits — it stamps system time, it never edits business facts) and returns that row's keys, with the INSERT of the new version selecting FROM that CTE. Feeding the INSERT from the UPDATE's RETURNING is what makes a replayed file a no-op; an unconditional INSERT leaves the partial unique index to raise 23505 instead, which is a backstop rather than idempotency. The pre-correction row stays readable either way, which is what makes the already-adjudicated claim defensible.

Follow-up

  • The claim adjudicated on 2026-03-03 is now retroactively uncovered. What does reprocessing have to read — the belief at adjudication time or the current truth — and what does the remittance reversal reference?
  • Add the constraint that stops two active coverage versions from overlapping in business time for the same person and plan, without blocking the superseded history rows.
  • A later file reinstates coverage with the same effective_date. How many rows exist for that period afterwards, and which one does query (a) return?

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

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

Worked solution (30 min)

  1. Run EXPLAIN (ANALYZE, BUFFERS) on the original and write down estimated versus actual rows for each predicate, which shows effective_date <= $2 is not selective.
  2. Add the generated column and rewrite the predicate to use coverage_end.
  3. Create the partial composite index and ANALYZE the table so the planner has statistics for the new column.
  4. Re-run EXPLAIN (ANALYZE, BUFFERS) and confirm an index scan with Rows Removed by Filter at or near zero.
  5. Measure the partial index size against the full-table equivalent with pg_relation_size to confirm the cache argument is real and not assumed.

Expected result: The (effective_date) index is rejected because effective_date <= $2 is a low-selectivity leading range predicate over 40M rows; the planner's estimated cost of random heap access exceeds a sequential scan. Fix: ALTER TABLE coverage_span ADD COLUMN coverage_end date GENERATED ALWAYS AS (COALESCE(termination_date, DATE '9999-12-31')) STORED; CREATE INDEX CONCURRENTLY idx_coverage_active ON coverage_span (enterprise_person_id, effective_date, coverage_end) WHERE valid_to = 'infinity'; and rewrite the last predicate as coverage_end >= $2. The new plan is an Index Scan on idx_coverage_active, Rows Removed by Filter near zero, buffers in the low tens rather than 1.8 million, and sub-millisecond execution.

Verify it yourself

  • Buffers drop from roughly 1.8 million to double digits — an order-of-magnitude change in buffers, not just in wall-clock time, is what proves the index is doing the work.
  • Rows Removed by Filter is 0 or near it; a large value means a predicate is still being re-checked per row and belongs in the index or the partial clause.
  • The rewritten and original predicates return identical rows for a member with an open-ended coverage and for one terminated exactly on $2.
  • pg_relation_size on the partial index is a small fraction of the same index built without the WHERE clause.

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'?

Describe how this medical device is supposed to work and draw all force diagrams.

Medium

Approach

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

Follow-up

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

How would you define the parameters when annealing a material?

Medium

Approach

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

Follow-up

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

Set rate limits across registration, batch ingest and bulk export

Medium · rate limiting · backpressure · prioritisation

Three callers share the identity resolution and longitudinal record APIs: a registration desk making 1-3x10^3 interactive match calls a second against a 200ms budget, a nightly ingest replaying millions of messages in a burst, and partner bulk exports walking whole panels. One global limit either starves registration during the batch window or stretches the batch past morning. Define the limiting scheme - the key, the algorithm, the reserved capacity, the response when a caller is limited - and state precisely what each of the three callers does when it is limited.

Approach

  1. Reject a single global limit, then reject per-IP: the batch runs from a handful of hosts and the desk sits behind a shared egress, so IP is simultaneously too coarse and too fine. Key on the authenticated principal plus a traffic class bound to the credential, never a class the caller declares in a header it controls.
  2. Pick the algorithm from the traffic shape. A fixed-window counter admits up to twice the limit across a window boundary, which is exactly the top-of-hour moment the desk bursts; a token bucket with a burst allowance, or a sliding-window counter, does not. The desk needs burst tolerance; the batch needs a steady ceiling.
  3. Give the classes different treatment rather than different numbers of the same thing: a reserved floor of capacity the interactive class cannot be pushed below, the remainder shared by batch and export, and the batch class shedding first under pressure. This is a scheduling decision - the class that can wait is the one that waits.
  4. Answer with 429 plus Retry-After in delta-seconds and the limit, remaining and reset headers, so the caller need not invent a backoff. Keep 429 distinct from 503: 429 means try later and the request did no work, 503 means the service is unhealthy. Conflating them makes correct client behaviour impossible to write.
  5. Write each caller's behaviour, because a limit without a documented client response only relocates the failure. The desk degrades to deterministic-only matching and flags the registration for review rather than blocking a patient; the ingest applies backpressure to its consumer, since the messages are durable and delay is free while loss is not; the export sleeps Retry-After and resumes from its cursor. Then state where the counter lives: a shared store with one atomic increment-and-expire per decision, sized so the limiter is not the bottleneck at tens of thousands of decisions a second, and with a stated fail-open or fail-closed behaviour when it is unreachable.

Worked solution (25 min)

  1. Write the three traffic profiles as numbers: request rate, burst shape, latency budget, and what each caller can tolerate on refusal.
  2. Choose the key, show where the class comes from on the credential, and write the check that stops a caller self-declaring.
  3. Work the fixed-window boundary arithmetic that admits twice the limit, then the token-bucket or sliding-window version that does not.
  4. Write the full 429 response with every header, and the exact backoff each of the three callers implements.
  5. Write the reserved-capacity rule and trace all three classes at 150 percent of total demand.

Expected result: Limit keyed on (principal, traffic_class) with the class bound to the credential rather than the request. Token bucket with per-class burst; the interactive class holds a reserved floor; batch sheds first. 429 with Retry-After in delta-seconds plus limit, remaining and reset headers, kept distinct from 503. On 429 the desk degrades to deterministic-only matching and flags for review, the ingest applies backpressure to its durable consumer, the export sleeps and resumes from its cursor. Counters in a shared store, one atomic increment-and-expire per decision, with a stated behaviour when that store is unreachable.

Verify it yourself

  • No caller can move itself into the reserved class by changing a request.
  • Two adjacent windows cannot together admit more than the intended rate.
  • At 150 percent demand the interactive class still meets its 200ms budget.
  • Every 429 tells the caller how long to wait without the caller guessing.

Follow-up

  • The batch runs under the same credential as an interactive tool. How do you separate them?
  • The limiter's shared store goes down. Does traffic fail open or closed, and what is the argument for your choice here specifically?
  • One partner's export is now 40 percent of read load while staying inside its limit. What do you change?

Duplicate patients created quietly by a matcher timeout fallback

Hard · partial outage · fallback policy · identity resolution

Over two days the manual duplicate-review queue grew eight-fold, and a clinician reports that a patient's prior results are missing from the chart. No error-rate alert fired: registration calls the identity resolution service with a 250ms timeout and, on timeout, creates a new enterprise person and returns 200. person_identity_link records match_score, link_status, decided_by and decided_at per link version. The identity service is operated by another team. Give the ordered investigation, what you can prove alone, and what you must ask them for.

Approach

  1. Work backwards from the damage rather than forwards from a dashboard. Take one duplicate pair, read both person_identity_link rows, and examine decided_by and match_score: a matcher version string, a user id, or the fallback's marker tells you which code path asserted the identity, and a NULL match_score means a deterministic link rather than a probabilistic one. If the duplicates were created by the fallback path, one query has given you the mechanism.
  2. Correlate decided_at for fallback-created links against your own client-side timeout counter, minute by minute. A creation rate that tracks the timeout rate with no lag is causal; one that does not sends you back to the first step rather than letting you build on a coincidence.
  3. Determine the failure shape. Is the timeout rate uniform across identity-service instances or concentrated on a few? Uniform means capacity or a global regression; concentrated means a partial outage — one degraded shard or replica — and the remedies differ. You can often infer this alone if responses carry an instance identifier; if they do not, that is the first thing to request.
  4. Name the real defect. The latency is the trigger; the incident is a fallback that converts unknown into new person. A 200 carrying a fabricated identity is worse than a 503, because it silently splits a chart, and the missing prior results are the direct consequence of that split rather than a separate problem.
  5. Stop the bleeding without blocking registration: on timeout, register under a provisional identity with link_status = potential_duplicate and route it to review. Clinical safety requires that registration proceeds; nothing requires that the system assert a new person on the strength of a timeout.
  6. Repair, then state the ask precisely. Because links are versioned and merges are reversible, remediation means re-running the matcher offline over the window's registrations and generating merge candidates — never rewriting downstream foreign keys. From the other team, request per-instance p99 and timeout counts for the window plus their deploy and index-maintenance log, and say which of your conclusions depends on that answer.

Follow-up

  • The missing results are attached to the losing identity. Describe the read path that surfaces them before the merge is approved, and what a clinician sees while the pair is still pending.
  • Your timeout is 250ms against a 200ms budget. What is the right value, and what happens to duplicate creation and to registration latency if you double it?

Suggested practice checklist

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

Day 1 — Rebuild the primitives by implementing them

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Day 7 — Solve out loud, under time

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

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

Outage answers are graded on instruments

Nobody is scoring your stamina at three in the morning. What carries weight is which signal told you something was wrong, what you measured before touching anything, what you rolled back versus what you fixed forward, and why you picked one. 'We restarted it and it went away' is a story about not knowing.

Give an example of the process you followed when making improvements to a manual process.

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

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

Tell me about your past medical or research projects.

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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

Tell me about a time you had a disagreement with your manager.

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Pick a story where you made the decision, not one where you watched it.
  3. Close with what you would do differently, concretely.

Follow-up

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

Where do you see yourself in 5 years?

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

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