cswork / libraryOpen practice workspace

Interview guides · CooperVision

CooperVision Software Engineer Interview Questions & Guide 2026

Testing practice for CooperVision software engineer candidates: the boundaries worth covering, and why flakiness is a real defect.

The 60-second brief

At CooperVision, the Software Engineer role is foundational to the company's mission of providing vision care products to a global market. You are not just writing code; you are building the digital infrastructure and technical systems that support CooperVision's manufacturing processes, research and development initiatives, and operational efficiency. Whether you are working on maintenance engineering for critical production lines or within CooperVision's R&D electronics teams, your work directly influences the precision and reliability of the company's medical devices.

The shape of the workload matters more for your prep than the industry label does. Read-heavy serving, write-heavy ingestion and scheduled batch processing have different binding constraints and fail in different places, so find out which one the team lives in before picking design topics.

Focus areas

  • Design person merges that remain reversible afterwards
  • Hold accumulator limits under concurrent claim adjudication
  • Authorise every identified read against current consent

Overview

This position is inherently collaborative and requires a unique blend of technical expertise and practical problem-solving. You will often find yourself working at the intersection of software, hardware, and physical production environments, requiring you to think about how your code interacts with real-world machinery and complex systems. It is an environment where precision is paramount, and the impact of your contributions is visible in the consistency and quality of the products delivered to patients worldwide.

The work is both challenging and rewarding, offering the opportunity to apply advanced technical skills to high-stakes industrial environments. You will be expected to navigate complex technical constraints, collaborate across multidisciplinary teams, and deliver robust solutions that keep CooperVision's operations running smoothly. For the right candidate, this role offers a rare chance to see how software engineering directly enables global manufacturing and health innovation.

The CooperVision interview process

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

Initial 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 Assessments

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.

Open Dialogue Interviews

An unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.

What to demonstrate

  • Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
  • Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
  • Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating

How to prepare

  • Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
  • Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
  • Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes

Team Member Interviews

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

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.

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.

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.

Saying 'eventually consistent' without naming the anomaly a user would see

Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.

Questions & practice

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?

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.

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?

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

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

Worked solution (20 min)

  1. Define the per-claim record and the three-way comparison on claim_version: greater resets, equal accumulates, lower discards.
  2. Run the stream once, touching only the claim map.
  3. Walk the claim map, skipping entries whose surviving version is a void, and add each remaining sum into a person map.
  4. Return the person map sorted descending by total.

Expected result: Claim A: version 1 has lines of 12000 and 8000 cents, version 2 (replacement) has lines of 9000, 8000 and 500. Claim B: version 1 has a single line of 5000, version 2 is a void carrying one zero-amount line. Both claims belong to one person. Your answer is 17500 cents, from claim A's version 2 only. The naive sum over all seven lines is 42500.

Verify it yourself

  • The number of claims contributing equals the number of distinct claim_ids whose highest version is not a void.
  • For a multi-line claim, the addends in its sum equal that surviving version's line count, not one.
  • Shuffling the input and re-running produces an identical result, which is the property the reset-on-higher-version rule buys you.
  • The all-rows sum is greater than or equal to your total, equal only when no claim has a version above 1 and none is a void.

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?

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

Denormalise consent into a decision table with a stated staleness bound

Hard · denormalisation · cache invalidation · access control

Access governance answers 'may this requester read this person's data for this purpose' on every identified read — tens of thousands per second at a 5ms p99 — from consent_directive: consent_id, enterprise_person_id, version, scope, purpose_of_use text[], grantee_org_id (NULL meaning all organisations), permit boolean, effective_ts, expires_ts, revoked_ts, status. Evaluating the normalised version history per read misses the budget. Design the denormalised decision store and its key, state the revocation staleness bound in seconds and the mechanism that actually enforces it, and say which normalised rows remain the system of record and why.

Approach

  1. Derive the shape from the read, not from the source table. The read is a point lookup on (person, grantee organisation, purpose), so fan the purpose_of_use array out into one row per purpose and make those three columns the primary key. A GIN index on the array would serve containment queries nobody issues.
  2. Handle the wildcard concretely. grantee_org_id NULL means 'all organisations', and NULL never equals anything, so a nullable column in the key breaks both the primary key and the equality lookup. Store a sentinel org id of 0 for the wildcard and resolve specific-before-wildcard in the lookup, or the wildcard row is silently unreachable.
  3. Make refusal a row, not an absence. An explicit permit = false must outrank a permit for the same key, so carry a precedence rank (specific grantee beats wildcard, refusal beats permit at equal specificity) and resolve with ORDER BY rank LIMIT 1. Default deny when nothing matches, so a rebuild failure fails closed.
  4. Separate the two staleness mechanisms and be precise about which one is the bound. The in-process decision cache has a TTL; a revocation also publishes an invalidation. The publish shortens the typical case to milliseconds, but it can be lost, so the guaranteed bound is the TTL alone — quote that number, for example 30 seconds, and justify it against the cache hit rate you need to hold 5ms p99.
  5. Evaluate expires_ts at read time against the request clock rather than baking the decision. A cached permit whose expires_ts has passed is wrong regardless of invalidation, because no event fires when a timestamp simply goes by.
  6. Keep consent_directive as the system of record: audit must reconstruct the directive in force at any past instant, the decision table holds only current state, and a corrupted decision table must be rebuildable by replaying the directives. Bound the write amplification by keeping grantees at organisation granularity and purposes a closed code set, so one directive expands to a known small number of rows rather than an unbounded cross product.

Follow-up

  • Break-glass must always succeed and must be unmistakably marked. Where does it sit relative to this lookup, and what stops it from becoming the quiet default path?
  • The invalidation bus is down for thirty minutes. Walk through what a revoked person's data exposure looks like minute by minute, and what the backlog does when the bus returns.
  • Where does the access audit record get written so that an audit-store outage degrades neither the log's completeness nor the read's availability?

Describe your process for diagnosing a failure in a system where software and hardware components interact.

Medium

Approach

  1. Say what you would check first and why it is the highest-information step.
  2. Work from the requirement backwards to the design.
  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?

How do you balance the need for rapid deployment with the necessity of thorough testing in a production environment?

Medium

Approach

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

Follow-up

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

Shape a bulk enrolment endpoint where rows fail independently

Medium · bulk operations · partial failure · idempotency

A payer partner submits weekly enrolment as one call carrying 10^4 to 2x10^6 member rows that become coverage_span (coverage_id, enterprise_person_id, payer_id, plan_id, subscriber_id, effective_date, termination_date, coverage_order, status, valid_from, valid_to, source_file_id). Some rows fail identity resolution, some carry a termination date before their effective date, and the rest apply cleanly. The partner's loader must be able to retry only what failed, and cannot fix the file until next week. Design the submission contract: synchronous or asynchronous, the response shape, how a row is addressed, and what retrying the failed subset does to rows that already applied.

Approach

  1. Size the call before shaping it. 2x10^6 rows will not survive a request/response cycle under any sane timeout, so the contract is asynchronous: upload, 202 with a job resource, poll or subscribe. All-or-nothing is equally wrong, because one malformed row would reject two million good ones and the partner cannot resubmit for a week.
  2. Give every row a caller-assigned stable identity - the partner's file id plus line number - so a result addresses back to the input without positional matching. Positional indexes break the instant the partner resubmits a filtered file, which is exactly what the retry is.
  3. Make the results a separate paginated resource, not an inline array: GET /enrolment-jobs/{id}/results?status=failed&cursor= returns one entry per failed row with a reason code and the offending field. An inline error array is unbounded, and two million failures is not a response body.
  4. Classify per row into applied, rejected and needs_review, and keep the last two addressable rather than dropping them. A row that failed identity resolution is not invalid data, it is an unresolved person, and it must be re-drivable after a manual link without the partner resubmitting anything.
  5. Make the row the unit of idempotency: UNIQUE (source_file_id, partner_line_number), so a resubmitted overlapping file is absorbed. State the one case that is not a duplicate - the same key with changed content must supersede, which in a bitemporal table means closing the prior version's valid_to and inserting a new row with valid_from set, leaving effective_date and termination_date as the partner asserted them rather than rewriting business time.

Worked solution (30 min)

  1. Decide the transport and show the arithmetic that rules out a synchronous body at two million rows.
  2. Write the job resource schema: state, counts by outcome, started and finished timestamps, and links to the result pages.
  3. Write the per-row result schema with the caller's row identifier, the outcome, the reason code and the offending field.
  4. Write the row-level idempotency key and its constraint, then trace a retry of the failed subset against a job where 90 percent applied.
  5. Write what happens to a row whose content changed since it applied: the valid_to close-out and the new version, with business dates untouched.

Expected result: POST /enrolment-files returns 202 with Location: /enrolment-jobs/{id}. GET on the job returns {state, counts:{applied, rejected, needs_review}, started_at, finished_at}. GET /enrolment-jobs/{id}/results?status=failed is cursor-paginated, one entry per input line keyed by (file_id, line_number) with reason_code and field. Row idempotency on UNIQUE (source_file_id, partner_line_number), so a retried subset re-applies nothing already applied. A changed row closes the prior version's valid_to and inserts a new version with valid_from, leaving effective_date and termination_date as asserted.

Verify it yourself

  • No response in the design grows with the number of input rows.
  • Every failed row is addressable by an identifier the partner assigned, not by position.
  • Retrying the failed subset twice leaves the same coverage_span state as retrying it once.
  • A row rejected for unresolved identity can be re-driven after a manual link without a partner resubmission.

Follow-up

  • Midway the job hits a run of failures caused by our own dependency being down. Does the job fail, pause or keep going, and what does the partner see?
  • The partner resubmits the entire file instead of the failed subset. What happens?
  • How does the partner learn that a row applied and was later superseded by a newer file, without diffing two full exports?

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

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

Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.

Day 1 — Coding, one pass at shallow depth

  • Solve one problem from each of six families, an array with two pointers, hash counting, binary search, a tree traversal, a graph traversal and one dynamic program, under a hard twenty-minute cap with no extensions, marking each finished, late, or stalled.
  • For every stall, write the exact move you could not make rather than the subject, so the note reads could not turn the recurrence into a loop rather than bad at dynamic programming.
  • Fix nothing today. The value of the pass is the unfixed record.

Deliverable: Six timed attempts marked finished, late or stalled, each stall carrying a named blocking move.

Day 2 — Design, one pass at shallow depth

  • Spend twenty minutes each on three different shapes, a read-heavy feed, a write-heavy ingest path, and something needing a transaction across two entities, stopping each at requirements, interface and data model.
  • After each, write the first question you could not answer, which is usually a number you could not estimate or a failure mode you had no vocabulary for.
  • Mark which of the three you would be most relieved not to be asked, and treat that as data rather than as a preference.

Deliverable: Three shallow designs, each with the first unanswerable question written at the bottom.

Day 3 — Fundamentals and the practical rounds

  • Answer eight short questions in writing at four minutes each, covering the material that fills the gaps between the big rounds: what happens between a URL and a rendered page, what an index costs on write, when a process is preferable to a thread, and what conditions a deadlock requires.
  • Do one thirty-minute practical task of the kind a take-home compresses: read an unfamiliar two-hundred-line file and write what it does, what you would change, and the one thing you remain unsure of.
  • Score every answer fluent, correct but slow, or absent, and keep the absent ones visible.

Deliverable: Eight scored short answers and one written reading of unfamiliar code.

Day 4 — The rounds that are about you, and the map

  • Deliver three behavioural answers aloud against a timer, a conflict, a failure you owned, and a decision made without enough information, marking any that ran past three minutes or contained no number.
  • Assemble the map: every marked item from days one to three on a single page, sorted by how likely it is to appear in your loop rather than by how uncomfortable it felt.
  • Choose exactly two areas for the remaining three days and write down what you are deliberately abandoning.

Deliverable: A one-page scored map of the whole surface area with two areas chosen and the rest explicitly abandoned.

Day 5 — First chosen area, to the depth you skipped

  • Work the higher-ranked area in four focused blocks, choosing items one level above where you stalled rather than repeating what already works.
  • After each block write the rule you extracted in one sentence with its precondition attached, since a rule carrying no precondition is exactly what fails under a variation.
  • Re-attempt the day-one or day-two item that exposed this area and compare against the original timing.

Deliverable: Four worked blocks, a timed re-attempt against the original, and three one-sentence rules with preconditions.

Day 6 — Second chosen area, where the gap is coverage rather than speed

  • Treat the second area differently from the first. Day five drilled something you could already half-do; this one is usually a topic you had simply never met, so build one worked reference example end to end and keep it, rather than attempting six problems badly.
  • Write down the vocabulary you were missing on day two or three, five terms at most, each with the one sentence that makes it usable in an answer rather than the textbook definition.
  • Redo the shallow attempt that exposed this area and note whether you now fail later in the problem, because moving the failure point is the realistic gain from a single day and is worth more than a score that did not change.

Deliverable: One worked reference example for the newly covered area, a five-term vocabulary list, and a note on where the failure point moved.

Day 7 — Reassemble the loop

  • Sit two rounds back to back with no gap, ordering them so the area you chose second comes last, because the map was built from rested, isolated attempts and the loop will reach your weaker area when you are already spent.
  • Write where the second round suffered from the first, which is normally the point at which structure collapses into narration.
  • Reduce the week to one page holding only the rules you can state without reading them.

Deliverable: Mock notes on cross-round carryover plus a one-page card of rules you can recite from memory.

Know where your numbers came from

Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.

What motivates you to work within the medical device and manufacturing industry?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. Name the disagreement and how you resolved it with evidence.
  3. Close with what you would do differently, concretely.

Follow-up

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

Describe a time you had to explain a complex technical issue to a cross-functional team member.

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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

Explain your experience with troubleshooting complex integrated systems.

Medium · behavioural and engineering judgement

Approach

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

Follow-up

  • What did you decide not to do, and why?
  • What would you do differently if you ran that again?

How do you handle disagreements with team members regarding technical direction?

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. Name the disagreement and how you resolved it with evidence.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

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