Interview guides · Cirtec Medical
Cirtec Medical Software Engineer Interview Questions & Guide 2026
Software engineer interview prep for Cirtec Medical: original coding, system design and debugging practice, each with a worked solution.
The 60-second brief
As a Software Engineer at Cirtec Medical, you serve as a critical bridge between complex medical technology and the life-enhancing devices that define the company’s reputation. You are not merely writing code; you are contributing to the development and maintenance of high-precision systems that must adhere to the highest standards of safety, quality, and regulatory compliance. Your work directly impacts the reliability of medical devices, ensuring they perform consistently in critical healthcare settings.
Ask whether any round happens inside an existing repository instead of a blank file. Reading unfamiliar code, isolating a fault and making the smallest correct change is a different skill from writing a function from scratch, and it needs its own practice.
Focus areas
- Make HL7 and X12 ingestion idempotent under replay
- Model bitemporal coverage and retroactive eligibility changes
- Authorise every identified read against current consent
Overview
This role requires a unique blend of technical expertise and a deep understanding of the Medical Device development lifecycle. You will often collaborate across departments—working closely with quality, manufacturing, and systems engineering teams to translate requirements into robust software solutions. Whether you are optimizing existing processes or architecting new features, your contributions help maintain Cirtec Medical’s position as a leader in the outsourced medical device manufacturing space.
The Cirtec Medical interview process
Cirtec Medical candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Phone Screen
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 Interview
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
In-Person Interview
You cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Where candidates lose points
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.
Writing the access audit record inside the read transaction, or firing it off after the response with no durability.
Inside the transaction, an audit-store outage blocks clinical reads and turns a logging dependency into a care outage. Fire-and-forget afterwards means the audit trail is incomplete during precisely the incidents it exists to reconstruct, and the gaps are invisible until someone asks for the log. The usual resolution is committing the access decision and its audit row together to a local outbox and shipping asynchronously, which keeps the read path available while preserving durability.
Reading the constraints as preamble rather than as part of the problem
The bounds are usually there to eliminate the obvious approach: n up to 10^5 makes an O(n^2) scan roughly 10^10 operations, far outside any per-test time budget, and an input larger than memory rules out loading it at all. When a bound is not given, ask for it, then say out loud which approach it kills.
Comparing floating-point values for equality, or holding money in them
Binary floating point cannot represent 0.1 exactly, so repeated addition drifts and an equality check fails on values that are mathematically equal. Store currency as integer minor units or a decimal type, and compare floats against a tolerance you chose for a stated reason.
Questions & practice
Sum surviving claim versions in one pass over unordered lines
Easy · hash map · versioning · streaming aggregation
You are streamed up to 50 million claim_line records in arbitrary order: claim_id, claim_version, line_number, frequency_code (original, replacement, void), enterprise_person_id, allowed_amount_cents. Every version of a claim shares its claim_id, and a replacement arrives as a higher claim_version. Return total allowed_amount_cents per enterprise_person_id, counting each claim once at its highest version and contributing zero when that version is a void. Amounts are non-negative int64 minor units. Target O(N) time and O(C) space for C distinct claims, single pass, no sort.
Approach
- Say the two grains out loud before writing anything: the version lives at claim grain, the money lives at line grain. Every wrong answer here comes from applying a claim-level rule with a line-level filter.
- Keep one hash map from claim_id to a small record (best_version, sum_cents, person_id, is_void). Per line: if claim_version is greater than best_version, reset sum_cents to this line's amount and overwrite the person and void flag; if equal, add to it; if lower, discard the line. That reset is what makes the pass order-independent, so a replacement arriving before its original still wins.
- Resolve the void at the end, not at ingest. A void only nullifies the claim if it is the surviving version, and zeroing on sight would let an earlier replacement that arrives later resurrect the money.
- Fold the C claim records into a person-keyed map in a second phase, O(C). Do not accumulate into the person total during the stream: you cannot subtract a superseded version you have already forgotten.
- Cost is O(N) time, O(C) space. The sort-based alternative, group by (claim_id, claim_version) and keep the max, is O(N log N) and needs the set resident; prefer it only when C approaches N and the map will not fit.
- Keep cents in int64 throughout. Fifty million lines at a realistic per-line ceiling stay four orders of magnitude below 2^63, so the integer type costs nothing and removes the float drift class entirely.
Follow-up
- You shard the stream across eight workers. Sharding by hash of claim_id works; what exactly breaks if you shard by enterprise_person_id instead?
- A replacement arrives for a claim_id whose original never appears in the batch. What does your map produce, and is that the financially correct answer or a reconciliation break?
- The same claim_id is reused by two different submitting organisations. How does the key have to change, and when would you have noticed?
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.
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?
Flatten overlapping coverage spans into a primary-payer timeline
Medium · sweep line · intervals · bitemporal
For one enterprise_person_id you hold up to 10,000 coverage_span rows: coverage_id, payer_id, plan_id, effective_date, termination_date (null means open-ended), coverage_order (1 is primary), status, valid_from and valid_to (system time). Given a system instant T and a date range D0 to D1, return disjoint business-date intervals covering that range, each labelled with the active coverage_id of lowest coverage_order, and with uncovered stretches returned explicitly. termination_date is the last covered day. Only status active counts. Target O(N log N) time, O(N) space.
Approach
- Apply the system-time slice before any interval logic: keep rows where valid_from is at or before T and valid_to is after T. That single predicate is what makes the answer 'what we believed at T' rather than 'what we believe now', and a retroactive termination loaded after T leaks straight into the output if you skip it.
- Convert each surviving row to a half-open day interval from effective_date up to termination_date plus one day, using positive infinity for a null termination. Half-open removes every off-by-one at the join between a termination and the next plan's effective date, which is where same-day switches get turned into false gaps.
- Sweep: emit 2N boundary events, sort by date in O(N log N), and hold the active set in a min-heap keyed by (coverage_order, coverage_id) with lazy deletion, popping the top while it has already expired at the current boundary. Each coverage is pushed once and popped once, so the sweep is O(N log N) total and O(N) space.
- Emit an output interval whenever the heap top changes between consecutive boundaries, and emit an explicit uncovered interval when the heap empties. A gap reported as a gap is a different answer from a gap omitted, and downstream the difference is a denial versus a silent assumption of coverage.
- Fix the tie-break and state it: lowest coverage_order, then lowest coverage_id. Two rows at order 1 is a data error, and without a deterministic rule the coordination-of-benefits answer changes between two runs over identical data.
- Clip to D0 and D1 last, so a coverage that starts before D0 still contributes its correct order inside the window.
Worked solution (30 min)
- Filter to rows live at T, then map each to a half-open interval and a (coverage_order, coverage_id) priority.
- Build the boundary list, sort it, and sweep with a lazily-deleted min-heap.
- At each boundary, drop expired heap tops and compare the new top to the previous one, emitting an interval on change.
- Emit an uncovered interval whenever the heap is empty between two boundaries.
- Clip the emitted list to D0 through D1 and assert the intervals are disjoint and contiguous.
Expected result: Window 2026-01-01 to 2026-12-31 with three live rows: coverage 1 at order 1 effective 01-01 terminating 06-30, coverage 2 at order 2 effective 03-01 open-ended, coverage 3 at order 1 effective 09-01 open-ended. Output is three intervals: 01-01 to 06-30 labelled coverage 1, 07-01 to 08-31 labelled coverage 2, 09-01 to 12-31 labelled coverage 3, with no gaps. Delete coverage 2 and the middle interval becomes an explicit uncovered stretch of 07-01 to 08-31.
Verify it yourself
- The emitted intervals are sorted, pairwise disjoint, and their union is exactly D0 through D1 once uncovered stretches are included.
- Every boundary date in the output is D0, the day after D1, some effective_date, or some termination_date plus one day. Any other date means a boundary was synthesised.
- Removing the lowest-order coverage promotes exactly the next order and leaves every boundary it did not touch unchanged.
- Choosing T before the load time of a retroactive termination returns the pre-correction timeline, and T after it returns the corrected one, from the same table.
Follow-up
- Run the same query at two system instants and diff the timelines. What did the correction change, and what does each extra instant cost you?
- A termination arrives with an effective date earlier than the service date of a claim that already adjudicated and paid. What does the timeline now say, and what has to happen to that claim?
- Two enrolment files disagree on coverage_order for a dependent. What does your tie-break do, and what should the system do instead of tie-breaking?
Return the displayable version of each analyte with a window function
Medium · window functions · versioned rows · indexing
observation_result is append-only: observation_id, enterprise_person_id, encounter_id, accession_id, placer_order_id, filler_order_id, loinc_code, value_numeric, unit_ucum, result_status in ('registered','preliminary','final','amended','corrected','entered_in_error'), collected_ts, issued_ts, version, supersedes_observation_id. A chart view needs the currently displayable value for each (accession_id, loinc_code) for one person over the last 90 days, excluding analytes whose newest version is entered_in_error but keeping analytes whose older versions were. Write the query, give the index that serves it, and say honestly which part of the plan the index cannot remove.
Approach
- Recognise the shape: this is a top-1-per-group over a version chain, not a filter. Pick the newest version first, then apply the status predicate to the winner — the order is the whole exercise.
- Use DISTINCT ON (accession_id, loinc_code) with ORDER BY accession_id, loinc_code, version DESC, observation_id DESC on PostgreSQL; the ORDER BY must start with the DISTINCT ON expressions or it is a syntax error. Use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY version DESC, observation_id DESC) with an outer WHERE rn = 1 if you need portability, since a window function cannot be referenced in the WHERE of its own query level.
- Push only person and time into the inner WHERE. Pushing result_status <> 'entered_in_error' inside promotes a retracted result's predecessor back onto the chart, which is the bug the prompt is testing for.
- Index (enterprise_person_id, collected_ts DESC) so the 90-day slice is an index range scan rather than a scan of the person's whole history.
- Be straight about the limit: because collected_ts carries a range predicate, no b-tree index can also deliver rows pre-ordered by (accession_id, loinc_code, version DESC), so the sort stays in the plan. That is acceptable because one person's 90-day slice is hundreds to low thousands of rows, and it is a quicksort in work_mem rather than an external merge — verify that rather than assume it.
Worked solution (30 min)
- Write the DISTINCT ON form with the full sort key and only person plus time in the inner WHERE.
- Wrap it in an outer query and apply the entered_in_error filter there, so it can only eliminate winners.
- Write the ROW_NUMBER equivalent and confirm both return identical rows against a fixture containing one preliminary, one final, one corrected and one retracted chain.
- Add the index, then read EXPLAIN (ANALYZE, BUFFERS) and confirm an index scan feeding a Sort node, with Sort Method reported as quicksort and no Disk usage.
- Construct the adversarial fixture: an analyte whose version 1 is entered_in_error and whose version 2 is final. It must appear in the output; an analyte whose version 2 is entered_in_error must not.
Expected result: SELECT * FROM ( SELECT DISTINCT ON (o.accession_id, o.loinc_code) o.accession_id, o.loinc_code, o.value_numeric, o.unit_ucum, o.result_status, o.collected_ts, o.issued_ts FROM observation_result o WHERE o.enterprise_person_id = :p AND o.collected_ts >= now() - interval '90 days' ORDER BY o.accession_id, o.loinc_code, o.version DESC, o.observation_id DESC ) latest WHERE latest.result_status <> 'entered_in_error'; with CREATE INDEX ON observation_result (enterprise_person_id, collected_ts DESC). The plan is an index scan on that index feeding a Sort feeding Unique; the sort cannot be indexed away because collected_ts carries a range predicate.
Verify it yourself
- Output row count equals SELECT count(DISTINCT (accession_id, loinc_code)) over the same window minus the number of chains whose newest version is entered_in_error.
- Every returned observation_id has no other row pointing at it via supersedes_observation_id within the window.
- All versions of one analyte share the same collected_ts; if they do not, the 90-day window can cut a version chain in half and return a stale winner.
- EXPLAIN (ANALYZE) reports Sort Method: quicksort with Memory in kilobytes, not external merge Disk — if it spills, the per-person slice is larger than assumed and the design needs revisiting.
Follow-up
- A correction arrives carrying the same filler_order_id and the same version number as the row it corrects. What breaks, and what do you add to the sort key?
- The same query for a cohort of 50,000 people instead of one person: does DISTINCT ON still hold up, and at what point do you materialise a latest-version table with a partial index?
- How would you show the clinician that the displayed value was amended, given the chart query returns only the winner?
Fix a paid-amount report that double counts claim versions
Medium · join fan-out · versioned claims · aggregation correctness
claim_line: claim_line_id, claim_id, claim_version smallint, line_number, frequency_code in ('original','replacement','void'), enterprise_person_id, coverage_id, billing_provider_npi, service_from_date, procedure_code, units, billed_amount_cents bigint, allowed_amount_cents, paid_amount_cents, adjudication_status, UNIQUE (claim_id, claim_version, line_number). A monthly report sums paid_amount_cents and joins coverage_span on enterprise_person_id with date overlap to attribute a plan. It overstates payment by roughly 9%, and reconciliation against remittance fails. Name both causes, then write the corrected query returning paid cents and plan per billing_provider_npi for March 2026 service dates.
Approach
- Cause one: the SUM spans every claim version. A replacement supersedes a prior claim and a void cancels one, so a replaced claim contributes twice and a voided claim contributes once when it should contribute nothing. Resolve the surviving version per claim_id before aggregating, never by deleting superseded rows.
- Cause two: the coverage join is one-to-many. coverage_span carries a system-time version per coverage period, so a person with three beliefs about one period multiplies every claim line by three. The join is for a label, not for filtering, so it must not be allowed to change cardinality.
- Collapse the version chain with DISTINCT ON (claim_id) ... ORDER BY claim_id, claim_version DESC, then join the lines back on (claim_id, claim_version), and exclude the surviving version when its frequency_code is 'void'.
- Make the coverage lookup cardinality-safe with a LEFT JOIN LATERAL ... LIMIT 1 pinned to valid_to = 'infinity' and ordered by valid_from DESC. LATERAL with LIMIT 1 cannot fan out by construction; a plain join can, and no amount of later DISTINCT repairs a SUM that already doubled.
- Keep the money in integer minor units the whole way. SUM(bigint) returns numeric in PostgreSQL, so there is no overflow and no binary floating-point error; cast to a display type only at the edge.
Follow-up
- A replacement arrives before its original. Your surviving-version logic picks max(claim_version) — what does the report show in the window before the original lands, and does the number self-correct?
- Add a claim_line_adjustment table with several reason codes per line. What does that do to this query, and where do you aggregate to keep it safe?
- Reconciliation is still off by 400 cents. Which of the two causes could still produce that, and what is your next query?
Can you describe the types of devices or systems you have worked on previously?
Medium
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
How do you approach problem-solving in a regulated environment?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Receive and verify asynchronous result notifications from a laboratory partner
Medium · webhooks · signature verification · ordering
A laboratory partner POSTs a notification to our webhook whenever an accession produces or corrects a result, and we then fetch the payload. Notifications arrive at least once, out of order, and are replayed in bulk when the partner restarts an interface. We must not act on a forged notification, must not process a replay as new work, and must not lose a notification whose accession we have never seen. Specify the endpoint contract: what is signed, what we verify, acknowledgement semantics, the replay window, and the consumer's behaviour for an unknown accession.
Approach
- Verify over the exact bytes received. HMAC-SHA256 over timestamp + '.' + raw request body with a shared secret, compared in constant time. Re-serialising the JSON before hashing breaks on key order, whitespace and unicode escaping, and a non-constant-time comparison leaks the expected signature byte by byte under timing.
- Bind the timestamp into the signed string and reject anything outside a narrow skew window such as 300s, so a captured notification cannot be replayed indefinitely. State the precondition honestly: the window is the partner's clock-skew budget, which is why it cannot be shrunk to nothing. Accept two secrets concurrently so rotation needs no coordinated cutover.
- Keep authentication and deduplication separate. A valid signature says the notification is genuine; it says nothing about whether we have processed it. Dedupe on UNIQUE (partner_id, notification_id) with INSERT ... ON CONFLICT DO NOTHING, the same atomic guard any at-least-once feed needs.
- Acknowledge fast, work later: verify, persist, return 200 inside the partner's timeout, and process asynchronously. Fetching and applying inline makes our processing latency their timeout and converts our slow dependency into their retry storm. Return 4xx only for what a retry cannot fix - 401 bad signature, 400 unparseable body - and 5xx for our own faults, because the partner's retry policy branches on exactly that line.
- Treat an unknown accession as ordinary traffic. Notifications routinely outrun the interface carrying the accession itself, so park the notification in a pending state keyed by accession and reconcile when it lands, with a stated maximum age that raises an alert. Rejecting it turns a recoverable ordering problem into permanent loss that nobody notices until a report is short.
Worked solution (30 min)
- Write the signed-string construction and the header format, then the verification in order: parse header, check timestamp skew, compute the HMAC over raw bytes, constant-time compare against each accepted secret.
- Write the DDL for the received-notification table with UNIQUE (partner_id, notification_id) and a processing-state column.
- Write the status-code table - which failures are 400, 401, 409 and 5xx - and next to each, what the partner's retry policy does with it.
- Trace a bulk replay of 50,000 notifications and state where they are absorbed and the cost per notification.
- Write the pending path: where a notification for an unknown accession waits, what triggers reconciliation, and the age at which it alerts.
Expected result: POST /webhooks/lab-results verified by HMAC-SHA256 over timestamp + '.' + raw body, constant-time compared, with a 300s skew window and two accepted secrets during rotation. Deduplication by UNIQUE (partner_id, notification_id) with INSERT ... ON CONFLICT DO NOTHING. Acknowledge 200 once persisted; process asynchronously. 401 bad signature, 400 unparseable, 5xx for our own faults so the partner retries with backoff. A notification whose accession is unknown parks in a pending table keyed by accession, reconciles on arrival, and alerts past a stated age.
Verify it yourself
- A byte-for-byte replay of an accepted notification does no new work and still returns 200.
- A body altered by one byte fails verification.
- The handler's p99 excludes the downstream fetch and apply.
- A notification arriving before its accession is retained rather than rejected.
Follow-up
- The partner replays six hours of notifications after a restart. What in the design stops any of it being re-applied?
- How do you rotate the signing secret without a coordinated deploy on both sides?
- A correction and its original arrive in the wrong order. What decides which value a chart shows?
Morning eligibility spike saturates the outbound payer pool
Medium · cache stampede · thundering herd · timeouts
The eligibility service caches answers keyed by (person, plan, service date) with a 24-hour TTL. Every weekday at 07:00 local, for roughly 90 seconds, p99 goes from 45ms to the 6s outbound timeout, the connection pool to external payers saturates, and payers begin returning 429. Cache hit rate dips to 55%. Payer-reported latency for the requests they do serve is unchanged. Name the mechanism, the evidence that separates it from a slow dependency, and the fix — including what you would not do to the TTL.
Approach
- Separate many distinct keys missing from one key missed many times concurrently. Log (key, outcome) and, over a ten-second window inside the spike, compute misses divided by distinct keys missed. A ratio near one is a cold cache and a capacity problem; a ratio of twenty or fifty is a stampede, where concurrent requests for the same key each perform their own origin fetch.
- Confirm the expiry is synchronised. Histogram the remaining TTL across live keys: a single absolute 24-hour TTL set during yesterday's 07:00 peak produces a spike in that histogram exactly 24 hours later, which is why the incident is punctual rather than proportional to load.
- Locate the saturation. Payer-side latency flat while client-observed latency climbs to the timeout means the queueing is on your side. Measure in-flight outbound requests against the pool maximum and the time spent waiting to acquire a connection. That is the evidence that rules out a slow dependency, and the 429s are a consequence of your fan-out rather than its cause.
- Remove the duplication first with single-flight coalescing keyed on (enterprise_person_id, plan_id, service_date): concurrent misses for one key make one origin call and the rest wait on its result. This alone collapses the misses-to-keys ratio and is independent of every other change.
- Then fix the synchronisation and bound the exposure. Use a TTL of minutes with plus or minus 20% jitter so expiries spread, plus a bounded stale-while-revalidate. State the worst case explicitly — a 300s TTL with a 10s stale window is at most 310s of staleness — and return the computed-as-of timestamp to the caller, because coverage terminates retroactively and a long TTL is a correctness decision, not a performance knob.
- Bulkhead the outbound path per payer so one slow or rate-limiting payer cannot consume the concurrency budget of requests destined for the others, and define what the service returns when it cannot reach the origin at all.
Follow-up
- What does the service return when the payer is unreachable and the cache is empty? Does registration proceed with an unverified flag or block, and who owns that decision?
- How would you warm the cache before 07:00 without the warm-up itself becoming the stampede — and which keys are even warmable given the service date is part of the key?
Suggested practice checklist
Roughly ninety minutes on weeknights with one longer weekend block. The plan cuts scope rather than compressing everything, on the assumption that one thing finished per night beats four half-started.
Day 1 — Fix the scope and take a cold baseline
- Read the role description and write the three things the loop will almost certainly test, then write an explicit not-doing list and keep it visible all week.
- Take one twenty-five-minute coding problem and one fifteen-minute design prompt cold, and write the single sentence naming what blocked each, because those two sentences decide where the remaining evenings go.
- Set the week's rule: one thing finished every night, including the night you only have forty minutes.
Deliverable: A one-page scope with a not-doing list and two cold attempts, each carrying one sentence on what blocked it.
Day 2 — One pattern, written three times from blank
- Choose the single pattern most likely to appear in your loop and write it three times from an empty file rather than editing the previous attempt.
- On the third pass, write the invariant as a comment before the loop body and the complexity before the first line of code.
- Stop at ninety minutes even if the third version is imperfect, and write the one thing you would fix given another hour.
Deliverable: Three independent implementations of the same pattern plus a note on what changed between them.
Day 3 — One design, only to the depth you can defend
- Take one system shape and go only as far as requirements, interface and data model, refusing to draw a box you could not survive a follow-up about.
- Attach one number to each non-functional requirement, deriving it rather than asserting it, and write the assumption the number rests on.
- Write the one tradeoff you are choosing against and the observation that would make you reverse it.
Deliverable: One design at interface-and-schema depth with derived numbers and one written reversible tradeoff.
Day 4 — Only the fundamentals you will have to defend
- Write, in under two hundred words each, the answers to the two questions that follow almost any implementation: why this structure and not the obvious alternative, and what happens to this code at a hundred times the input.
- Write what an index actually costs: faster lookups on the indexed columns against a write that now maintains a second structure, plus the cases where the planner declines to use it anyway, low selectivity, or a predicate wrapping the column in a function.
- Delete any answer you cannot deliver aloud in under a minute, since an answer that needs reading is not an answer you have.
Deliverable: Three written answers, each under two hundred words and each timed aloud.
Day 5 — Your own work, timed
- Write a ninety-second and a four-minute version of your main project and time both aloud rather than reading them.
- Prepare the two follow-ups that always come: what you would do differently, and how you knew it worked.
- Put one number in the first sentence and be ready to say exactly where it came from and what it excludes.
Deliverable: Two timed narratives with one defensible number in the opening line.
Day 6 — The one full rehearsal, in the weekend block
- Run a sixty-minute mock covering a coding round and a design round in one sitting with no break, because sustained attention is the thing evenings have not trained.
- Immediately afterwards, and before hearing any feedback, write the three moments you lost the thread.
- Spend the rest of the block only on those three moments, and on nothing you merely feel shaky about.
Deliverable: Mock notes naming three failure moments with a specific fix written under each.
Day 7 — Taper
- Write the twenty-minute warm-up you will actually do on the morning: one problem you can already solve from a blank file, one design you can narrate, and nothing you have never seen.
- Re-read only your own notes from this week and open no new material.
- Write the logistics down: the editor or shared document you will be working in, whether execution and lookups are permitted, and the sentence you will use when you do not know something.
Deliverable: A one-page card holding the design structure, the project numbers, and the logistics.
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.
How do you handle changing priorities in a fast-paced engineering setting?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- 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.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Describe a time you had to handle conflict with a colleague.
Medium · behavioural and engineering judgement
Approach
- 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.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
How do you handle stress or stay organized when working against tight deadlines?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- 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?
Tell me about a process that you improved.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?