cswork / libraryOpen practice workspace

Interview guides · Arrowhead Pharmaceuticals

Arrowhead Pharmaceuticals Software Engineer Interview Questions & Guide 2026

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

The 60-second brief

A Software Engineer at Arrowhead Pharmaceuticals plays a pivotal role in the intersection of high-stakes biotechnology and rigorous quality engineering. You are not just writing code; you are building the infrastructure and validation frameworks that ensure the safety, efficacy, and reliability of groundbreaking RNAi-based therapeutics. Your work directly supports the teams responsible for analytical method validation, ensuring that Arrowhead Pharmaceuticals' data-driven processes meet pharmaceutical industry standards.

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

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

Overview

This position is inherently collaborative, requiring you to bridge the gap between complex technical requirements and the operational needs of scientific teams. You will contribute to the development of systems that maintain data integrity and support the lifecycle of pharmaceutical development. If you are driven by the prospect of applying your engineering expertise to solve critical problems in a high-impact, science-led environment, this role offers a unique opportunity to influence the future of medicine.

While the role title may focus on Quality Engineering, your core software engineering skills—logical problem solving, system architecture, and meticulous documentation—are the primary drivers of your success here.

Where to focus your preparation

Treat the sections below as preparation areas and confirm the format with your recruiter.

Preparation focus

No round sequence has been reported for this company, so work the categories below and confirm the format with your recruiter.

What to demonstrate

  • Breadth across SQL, experimentation and product reasoning
  • Ability to state assumptions before choosing a method

How to prepare

  • Drill the practice exercises below and time yourself
  • Prepare three quantified stories about decisions you drove

Where candidates lose points

Treating a medical record number or a member ID as a globally unique key and joining on it directly.

These identifiers are unique only within the authority that issued them. Two facilities in one network routinely have the same medical record number for different people, and member IDs get reissued when someone changes plans. Joining on the bare value merges two patients' records, which is the most damaging failure available in this domain, and it passes every test written against a single-facility fixture because the collision only appears once a second source is connected.

Storing monetary amounts as floating-point numbers.

Binary floating point cannot represent values like 0.10 exactly, so summing allowed and paid amounts across millions of claim lines accumulates representation and rounding error. Reconciliation against a remittance file is exact to the cent, so the drift surfaces as a balance that is off by a few cents with no identifiable cause, and engineers lose days looking for a logic bug in code that is correct apart from its numeric type. Integer minor units or an exact decimal type removes the class entirely.

Going silent while thinking

Narrate the candidates and why you are discarding them, even in fragments: sorting first would make this a two-pointer scan, but it destroys the original indices, which the output needs. From the other side of the table, a candidate thinking hard and a candidate stuck are indistinguishable until one of them speaks.

Sorting when the problem never required a total order

Match the algorithm to the guarantee actually needed: the top k comes from a size-k heap in O(n log k) time and O(k) space, distinctness needs a set rather than an ordering, and a small bounded integer key range admits a linear counting pass. A full O(n log n) sort is the right default only when you genuinely need everything in order.

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?

Resolve enterprise identity from a link log that supports unmerge

Hard · union-find · rollback · identity resolution

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

Approach

  1. Intern the node key as the pair (assigning_authority, source_person_id) into a dense integer index. Never key on source_person_id alone: two facilities in one network routinely issue the same medical record number to different people, so a bare-value key merges two patients before any matching logic has run, and a single-source test fixture will never show it.
  2. Classify the log rows before touching the structure. auto_linked and manual_linked are unions; potential_duplicate and rejected are recorded decisions that must not union anything; unlinked is a revert of the link it supersedes, not a new edge.
  3. Use union by size with an explicit undo stack, pushing (attached_root, its previous parent, the previous size of the absorbing root) on every union. Find walks parent pointers to the root: union by size bounds tree height at log2(n), so a query is O(log n) worst case, a union is O(log n), and a revert pops two words and is O(1).
  4. Path compression is the thing you have to give up, and the reason is specific: it rewrites parent pointers of nodes that were never named in the union being recorded, so the undo entry no longer describes the mutation that happened and a revert restores a forest that is quietly wrong. Near-constant find is only available to an append-only structure.
  5. Hold enterprise_person_id as a property of the root slot rather than a value copied onto every member. A merge then relabels one slot instead of n rows, and an unmerge restores two labels instead of reconstructing n.
  6. State the price plainly: every read path gains a resolution hop and no downstream table can carry a plain patient_id column. That cost is what buys reversibility, and it belongs in the design discussion rather than being discovered later by whoever writes the first join.

Worked solution (40 min)

  1. Intern three nodes: A is (AUTH1, MRN-77), B is (AUTH2, MRN-77), C is (AUTH1, MRN-90).
  2. Apply union(A, C) from a manual_linked row, pushing its undo entry.
  3. Apply union(A, B) from an auto_linked row with a high match score, pushing its undo entry.
  4. Query all three, then pop one undo entry and query all three again.
  5. Assert the parent and size arrays element-wise against a snapshot taken before the second union.

Expected result: After the first union, A and C share a root and B is its own root. After the second, all three share a root. After reverting one union, B is its own root again and A and C remain merged, with the first union's record untouched. Note what the key bought you: A and B collide on MRN-77 and would have been one identity from the first insert under a bare-value key, with no decision row ever recorded.

Verify it yourself

  • After reverting all k unions, the parent and size arrays are identical element by element to the pre-union snapshot, not merely equal in their query answers.
  • Tree height never exceeds log2(n). Assert it, because one union that attaches by arbitrary root instead of by size breaks the find bound without any visible symptom.
  • Applying a potential_duplicate or rejected row changes no query answer and pushes nothing onto the undo stack.
  • Querying (AUTH1, MRN-77) and (AUTH2, MRN-77) before any link row returns two distinct identities.

Follow-up

  • A merge is found wrong three weeks and 200,000 unions later, and your stack only reverts a suffix. What do you do instead, and what does that cost?
  • A claim was posted under the losing identity before the merge. After the unmerge, which identity owns it, and what in your design let you answer that?
  • Two matcher instances submit unions concurrently. What is the smallest change that keeps both the structure and the undo stack consistent?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Clip to D0 and D1 last, so a coverage that starts before D0 still contributes its correct order inside the window.

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

  1. Recognise the shape: this is a top-1-per-group over a version chain, not a filter. Pick the newest version first, then apply the status predicate to the winner — the order is the whole exercise.
  2. Use DISTINCT ON (accession_id, loinc_code) with ORDER BY accession_id, loinc_code, version DESC, observation_id DESC on PostgreSQL; the ORDER BY must start with the DISTINCT ON expressions or it is a syntax error. Use ROW_NUMBER() OVER (PARTITION BY ... ORDER BY version DESC, observation_id DESC) with an outer WHERE rn = 1 if you need portability, since a window function cannot be referenced in the WHERE of its own query level.
  3. Push only person and time into the inner WHERE. Pushing result_status <> 'entered_in_error' inside promotes a retracted result's predecessor back onto the chart, which is the bug the prompt is testing for.
  4. Index (enterprise_person_id, collected_ts DESC) so the 90-day slice is an index range scan rather than a scan of the person's whole history.
  5. Be straight about the limit: because collected_ts carries a range predicate, no b-tree index can also deliver rows pre-ordered by (accession_id, loinc_code, version DESC), so the sort stays in the plan. That is acceptable because one person's 90-day slice is hundreds to low thousands of rows, and it is a quicksort in work_mem rather than an external merge — verify that rather than assume it.

Follow-up

  • A correction arrives carrying the same filler_order_id and the same version number as the row it corrects. What breaks, and what do you add to the sort key?
  • The same query for a cohort of 50,000 people instead of one person: does DISTINCT ON still hold up, and at what point do you materialise a latest-version table with a partial index?
  • How would you show the clinician that the displayed value was amended, given the chart query returns only the winner?

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

Design an error taxonomy for a real-time eligibility check

Easy · error taxonomy · timeouts · graceful degradation

The registration desk calls GET /eligibility?person_id=&plan_id=&service_date= before a visit. The service answers from locally held coverage_span rows or by an external inquiry that takes seconds and sometimes fails outright. Staff must then proceed, proceed with an unverified flag, or stop, and the screen cannot make that call on its own. Define the response and error taxonomy: which outcomes are 2xx with a payload, which are 4xx, which are 5xx, what each carries, which are safe to retry, and what the desk is instructed to do for each. State the timeout budget.

Approach

  1. Separate the three things a boolean conflates: covered, not covered, and could not determine. Only the first two are answers. Flattening the third into 'not covered' produces a wrong financial answer wearing the clothes of a determinate one, and the desk cannot tell the difference.
  2. Put every determinate and indeterminate answer on 200 with a discriminated payload: {determination: covered | not_covered | indeterminate, reason_code, as_of, computed_at, source: local | payer | cache, coverage_order, cost_share, next_action}. The transport status describes the call; the payload describes the coverage. A not_covered answer is a successful call.
  3. Reserve 4xx for caller faults a retry cannot fix and make them distinguishable, because the remedy differs: 404 unknown person, 422 malformed or out-of-range service_date, 403 when the access decision refuses the purpose of use. Nothing retryable belongs in 4xx.
  4. Treat an external timeout as an outcome, not a transport failure. Return 200 with determination=indeterminate, reason_code=payer_timeout, and the locally derived coverage_span answer as of service_date alongside it. A 504 here throws away a usable degraded answer the desk could act on. Keep 5xx for the case where even the degraded answer could not be produced.
  5. Fix the budget end to end and state who enforces it: a roughly 2s desk interaction budget, a hard 1500ms deadline on the external inquiry, the remainder for the local path, and a client timeout set strictly above the server's so the server decides the fallback rather than the client abandoning a call that was about to degrade correctly. Note that every outcome here is retry-safe because the call is a read whose only side effect is the access audit row.

Worked solution (20 min)

  1. Write the full payload for a covered answer, including as_of, computed_at and the coverage_order that drives coordination of benefits.
  2. Enumerate every failure the call can hit - unknown person, bad date, consent refusal, no local data, payer timeout, payer error, our own fault - and assign each a status code and a reason code.
  3. For each row write the one sentence the desk sees and the action offered, then check that no two causes collapse into the same action.
  4. Do the deadline arithmetic: total budget, external deadline, remaining local budget, and the client timeout that must exceed the server's.
  5. State the retry rule per class: retryable, not retryable, or retryable with backoff only.

Expected result: 200 for covered, not_covered and indeterminate, each carrying {determination, reason_code, as_of, computed_at, source, next_action}; 404 unknown person; 422 malformed or out-of-range service_date; 403 consent or purpose-of-use refusal; 502 or 504 only when not even the degraded local answer could be produced; 500 for our own faults. A payer timeout is 200 + indeterminate + payer_timeout, carrying the local coverage_span answer as of service_date. External deadline 1500ms inside a 2s desk budget, with the client timeout set above it.

Verify it yourself

  • No single field doubles as both the transport result and the coverage answer.
  • Every 4xx names an input the caller can change, and nothing retryable is a 4xx.
  • An indeterminate answer still carries an as_of date and whatever local coverage was readable.
  • The client timeout is strictly greater than the server's external deadline, so degradation is the server's decision.

Follow-up

  • Two payers answer and disagree about which is primary. What does the endpoint return?
  • How long may an eligibility answer be cached, and what exactly is the cache key?
  • The desk proceeded on an indeterminate answer and the claim denies six weeks later. What in the response makes that decision reconstructable?

Eligibility reads with an as-of date and a payer timeout

Easy · caching · invalidation · degraded-mode · timeout-budget

The eligibility service answers whether a person is covered for a service date and what they will owe. Peak is roughly 4,000 requests per second between 07:00 and 10:00 across several time zones. Locally held coverage_span rows (enterprise_person_id, payer_id, plan_id, effective_date, termination_date, status, valid_from, valid_to) answer most calls, but a real-time inquiry to the external payer takes one to eight seconds and fails outright a few times an hour. Specify the cache key, the time-to-live, the timeout budget, and exactly what registration receives when the payer is unreachable.

Approach

  1. Key the answer on (enterprise_person_id, payer_id, plan_id, service_date) and carry the as_of instant inside the value. Coverage terminates retroactively, so the same tuple can be covered and not covered for the same service date, differing only in when the answer was computed; a key without service_date lets today's answer serve tomorrow's question.
  2. Set the time-to-live from the exposure you are willing to own rather than from the hit rate. Enrolment files land daily to weekly, so a retroactive termination becomes knowable within a day; a 15-minute entry bounds the window in which you keep serving an answer you have already been told is wrong, and costs roughly one origin call per member per morning. Pair it with an explicit invalidation driven by the enrolment loader publishing changed person identifiers.
  3. Budget the external round trip as a chain that fits inside the caller's deadline: compute the local answer first, then a 2-second cap on the payer inquiry inside a 2.5-second caller deadline. The external call is an upgrade to the local answer, not the origin of it.
  4. Decide the degraded behaviour explicitly and label it in the response: return the locally derived answer with its source and as_of set, never a fabricated 'covered'. Registration proceeds with an unverified flag and the encounter is queued for re-verification. Blocking registration on a partner's outage converts someone else's incident into a care-delivery incident.
  5. Name the trade-off rather than implying there is none: you are choosing availability and accepting bounded staleness, and the cost lands weeks later as retroactive denials. Quantify it as unverified encounters per outage minute so it is a decision someone signed rather than a default nobody chose.

Follow-up

  • The payer responds 'active' while your local coverage_span says terminated effective three days ago. Which answer wins, and what do you store?
  • A cache flush at 07:15 sends every registration desk to the origin for the same plan at once. How do you hold it to one origin call per key?

Gateway instances OOM every five days with flat traffic

Medium · memory leak · heap profiling · idempotency

Interoperability gateway instances are OOM-killed with exit 137 every five to six days. Resident memory grows about 400MB a day per instance and never falls; restarting resets it. Message throughput, connection count and partner mix are unchanged over that period. The gateway keeps an in-process set of recently seen idempotency keys — (sending facility, sending application, message control ID, event timestamp) — in front of the durable ledger. Give the ordered investigation and say what evidence would rule the idempotency cache out.

Approach

  1. Establish that this is a retained-object leak before profiling. Compare resident memory with live heap measured immediately after a forced collection, sampled daily. Resident memory rising while post-collection live heap stays flat points at allocator fragmentation, off-heap or native buffers, or thread stacks, and a heap dump will implicate the wrong object. Rising post-collection live heap is a genuine leak.
  2. Find what the growth is linear in. Plot daily growth against messages processed, connections accepted and wall-clock hours across instances carrying different traffic. Proportional to messages means something is retained per message; proportional to connections means per-connection state is not released on abnormal close, such as a half-open socket whose FIN never arrives.
  3. Diff two heap dumps taken six hours apart on the same instance, sorted by retained size, and identify the dominator. An unbounded set or map shows up as one root holding millions of small entries. This is a five-minute answer once you have the dumps, which is precisely why the first two steps exist: they tell you whether the dump can answer the question at all.
  4. Check the idempotency cache directly against arithmetic rather than suspicion. At single-digit millions of messages a day and a key of roughly 80 bytes plus per-entry overhead, an unevicted set grows by hundreds of megabytes a day, which matches the observed 400MB. A live gauge of its entry count that stays flat is the evidence that rules it out; a gauge tracking messages processed convicts it.
  5. Fix and restore the invariant: bound the cache by size or by a time window, and keep the durable unique constraint as the authority. An in-process set is a latency optimisation and cannot be the correctness mechanism, because it does not survive a restart and is not shared across instances — so a replay right after a deploy would bypass it entirely.
  6. Verify over 48 hours that post-collection live heap is flat, and alert on the cache's entry-count gauge rather than on resident memory, which only tells you after the growth has already happened.

Follow-up

  • How long must the time window be? Tie the number to the sender's replay behaviour after an interface restart and to how far back the broker redelivers.
  • If growth had tracked connections instead of messages, what would you look at first, and how would a half-open MLLP connection show up in the socket table?

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.

'We rewrote it in a better framework' is not a story

A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.

How do you work on a team?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

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

Describe a time you had to explain a technical concept to a non-technical stakeholder.

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. Close with what you would do differently, concretely.
  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 a time you had to pivot your approach due to changing requirements.

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 would you do differently if you ran that again?
  • What did you decide not to do, and why?

How do you handle disagreements regarding technical implementation?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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