cswork / libraryOpen practice workspace

Interview guides · Amica Insurance

Amica Insurance Software Engineer Interview Questions & Guide 2026

Amica Insurance software engineer prep on operations: decide what you would need to see to know a release is going wrong.

The 60-second brief

A Software Engineer at Amica Insurance plays a pivotal role in maintaining the company’s reputation for excellence and reliability. You will be responsible for designing, developing, and maintaining the robust software systems that support Amica Insurance’s core business operations, including policy management, claims processing, and customer service platforms. Your work directly impacts how millions of policyholders interact with the company, making technical precision and system stability paramount.

If the loop includes an asynchronous take-home, treat it as a code review of you rather than as a puzzle: structure, what you chose to test, and what you wrote down about the constraint you were working under. Hold the stated time box and say what you would have done with more of it.

Focus areas

  • Reconcile the ledger against processor settlement files
  • Name the isolation level each invariant requires
  • Make every money-moving endpoint idempotent by key

Overview

This role is both challenging and rewarding, often involving work with complex enterprise-level software such as Guidewire and ClaimCenter. You will collaborate with cross-functional teams to modernize legacy systems, implement new business rules, and ensure that digital solutions are scalable and secure. If you are a problem-solver who enjoys working on high-impact projects that require a deep understanding of both clean code and business logic, this position will offer you significant professional growth.

The Amica Insurance interview process

Amica Insurance candidates report 5 rounds · ≈ 4-6 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

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What to demonstrate

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Multi-Part Interviews

Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.

What to demonstrate

  • Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
  • Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
  • Whether you can be redirected in the middle of an answer without restarting it from the beginning
  • Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it

How to prepare

  • Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
  • Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
  • Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing

Manager Involvement

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.

Final Offer

Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.

What to demonstrate

  • Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
  • Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
  • Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
  • Whether you can say which calls you made alone and which you escalated, and why the line sat where it did

How to prepare

  • Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
  • Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
  • Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap

Where candidates lose points

Retrying a charge after a timeout

A timeout is not a failure; it is an unknown outcome, and the request may have been processed in full with only the response lost. Re-sending it without an idempotency key that the processor itself honours produces a duplicate charge, which is a customer-visible incident and usually a dispute. The correct handling is to treat the state as unknown, query the processor for that key or client reference, and only then decide. The mechanism also depends on the key being generated once by the caller and reused across every attempt — generating a fresh key per retry turns the whole scheme into a no-op while leaving all the code that appears to implement it in place.

Calling a compensating action a rollback

A saga's compensation is a new, externally visible business event, not an undo. A refund after a capture leaves both movements on the customer's statement, may not return the scheme fee, and lands days later rather than immediately. Designing a multi-service flow as though the compensation restores the prior state produces flows that turn out to be unimplementable at the final step, when the thing that needs undoing has already left the building. The sequence has to be ordered so the irreversible step is last and the reversible ones precede it, with an explicit pending state shown to the customer while a compensation is in flight.

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.

Assuming fixed-width integer arithmetic cannot overflow

In languages with fixed-width integers, including C, C++, Java, Go and Rust, computing a midpoint as (lo + hi) / 2 overflows once the sum passes the type's maximum, so write lo + (hi - lo) / 2 instead. Say which language you are in: arbitrary-precision integers, as in Python or Ruby, remove this specific hazard and none of the others.

Questions & practice

Parse settlement amounts into minor units without floating point

Easy · parsing · money representation · iso-4217

A settlement file carries amounts as text: 1234.56, -0.07, 1,234.5, 1234, 12 345,67, and for three-exponent currencies 1.234. Each line also carries an ISO 4217 alphabetic code and the file's sign convention. Write the function converting one amount string plus its currency into a signed amount_minor int64, scaling by the currency's ISO 4217 exponent — 0 for JPY and KRW, 2 for USD and EUR, 3 for KWD and BHD. Reject anything not convertible exactly, with a typed reason. Never raise, never use a float.

Approach

  1. Fix the contract first: return a result union of Ok(int64) or Err(reason) with reason drawn from unknown_currency, malformed, too_many_fraction_digits, overflow. A parser on this path that throws turns one bad line into a failed fifty-million-line run.
  2. Decide which character is the decimal mark from the file's format spec, not from a heuristic on the last separator, then strip grouping separators. 1,234 is genuinely ambiguous between 1234 and 1.234 exactly when the exponent is 3, so an unstated spec is a reject, not a guess.
  3. Split on the decimal mark. Right-pad the fraction with zeros to the currency's exponent e; if it is longer than e and any dropped digit is non-zero, return too_many_fraction_digits. Silently truncating is how half a minor unit becomes a reconciliation break nobody can source.
  4. Build the integer by digit accumulation on int64 with checked multiply and add, or equivalently concatenate the integer and padded-fraction substrings and parse once. Either way the value never passes through a float.
  5. Apply the sign last, from the convention the file declares — trailing CR/DR, a trailing minus as in 1234.56-, or a leading minus. Assuming a leading minus silently flips every credit in a file that uses the trailing form.
  6. Complexity is O(len) per amount with no allocation beyond a digit buffer, so the whole file is one O(total bytes) pass.

Worked solution (20 min)

  1. Write the exponent table {JPY:0, KRW:0, USD:2, EUR:2, KWD:3, BHD:3} and the result type before any parsing code.
  2. Implement integer-only scaling: split on the decimal mark, pad or reject the fraction, concatenate, parse once with checked arithmetic.
  3. Table-test: (1234.56, USD) to 123456; (1234, JPY) to 1234; (1.234, KWD) to 1234; (1234.56, JPY) to Err(too_many_fraction_digits); (0.005, USD) to Err; (-0.07, USD) to -7.
  4. Write the float implementation beside it and run both over ten thousand random amounts across all six currencies, printing every disagreement.
  5. Round-trip: format amount_minor back to text at the currency's exponent and assert equality with the normalised input.

Expected result: All six table cases pass. The float version agrees with the integer version on ordinary two-decimal USD amounts — which is exactly why it survives review — and disagrees on every JPY amount by 100x, every KWD amount by 10x, every input with more fraction digits than the exponent (which it rounds instead of rejecting), and under truncation, where int(1.15 * 100) yields 114.

Verify it yourself

  • For every input that parses, formatting the result back at the currency's exponent reproduces the normalised input string.
  • No code path returns a value when the fraction is longer than the exponent and a dropped digit is non-zero.
  • The function body contains no float, no double and no %f — grep is a legitimate test here.
  • The sum of parsed minor units over a whole file equals the file's own control total exactly, with no tolerance applied.

Follow-up

  • Exponent 4 exists (CLF). Does the exponent come from a compiled-in table or from the file header, and what happens when the two disagree?
  • A line has more fraction digits than the exponent allows. Is that a reject, a round, or a break — and who makes that call?
  • How do you fuzz this so that every failure is a typed rejection rather than a wrong number?

Derive per-account balances and catch unbalanced transactions

Easy · aggregation · double-entry · streaming

You are given ledger_entry rows streamed in entry_id order: transaction_id, account_id, direction (debit or credit), amount_minor (a positive int64), currency, business_date. Up to 500 million rows, at most 20 million distinct (account_id, currency) pairs, and the entries of one transaction are contiguous in the stream. In a single pass with no re-reads, return the closing balance per (account_id, currency) and the transaction_id of every transaction whose entries do not sum to zero within each currency. State your time and space bounds.

Approach

  1. Normalise the sign at read time from direction, not from the amount: signed = +amount_minor for debit, -amount_minor for credit (state which convention you picked). The schema constrains amount_minor > 0 precisely so the sign lives in exactly one place.
  2. Hold one hash map keyed (account_id, currency) to an int64 running total. Twenty million keys at 16 bytes of payload plus map overhead is order 1 GB in most runtimes — quote the number, and offer the fallback: partition the stream by hash(account_id) % P and run P passes for 1/P of the memory.
  3. Ride the zero-sum check on the same pass. Because a transaction's entries are contiguous, keep a tiny currency -> int64 map for the current transaction_id only, test it against zero on the boundary and at EOF, then clear it. That is O(currencies in one transaction), typically one or two.
  4. Bound the arithmetic explicitly. Int64 holds about 9.22e18, so overflowing one account across 500 million entries needs an average of 1.8e10 minor units per entry — safe here, but use a checked add so an adversarial file fails loudly rather than wrapping.
  5. Complexity: O(n) time, O(distinct account-currency pairs) space, one sequential pass, no sort. The zero-sum check adds no asymptotic cost, which is the argument for doing it here rather than in a second job.

Follow-up

  • Entries of a transaction are no longer contiguous. What does the zero-sum check cost now, and which is cheaper: buffering open transactions or an external sort on transaction_id?
  • How would you produce the same balances as of an arbitrary business_date without a second full scan?
  • The job is restarted after a crash halfway through the file. What makes the second run produce identical output?

Compute peak held exposure from overlapping authorisation holds

Medium · sweep line · intervals · authorisation holds

An account has up to 2 million authorisations on one business date: (auth_id, amount_minor, created_at, expires_at) with the hold live over the half-open interval [created_at, expires_at), plus capture events (auth_id, captured_minor, captured_at) that reduce the hold at captured_at, and explicit reversals that drop the remainder to zero. Timestamps are microsecond-precision timestamptz. Return the maximum total held amount across the day and the earliest instant it is reached, with complexity. Then say what changes if the deliverable is the peak per minute instead.

Approach

  1. Expand each authorisation into signed delta events rather than reasoning about intervals: +amount at created_at, -remaining at expires_at, -captured_minor at each captured_at, -remaining at reversed_at. The problem collapses to a running sum over a sorted event list.
  2. Sort the 2n to 4n events by (timestamp, sign) with negative deltas ordered first on a tie. The half-open convention forces that: at exactly expires_at the hold is already gone, so a - must land before a + at the same instant or you report a one-microsecond peak that never existed. O(n log n) time, O(n) space.
  3. Sweep once, tracking running, best and best_at, taking the first instant that attains the maximum. Say out loud which tie rule you are using — 'the peak' is ambiguous when the same level is reached twice, and the caller needs to know which instant they are being handed.
  4. If timestamps are bucketed (1,440 minute buckets for the per-minute variant), drop the sort for a difference array: add the delta at the start bucket, subtract at the end bucket, prefix-sum once. O(n + B) time and O(B) space, strictly better, at the cost of answering only at bucket resolution.
  5. Assert the invariant during the sweep: running must never go negative. A negative total means a capture exceeded its authorisation, which is an invariant violation upstream rather than a sweep bug — fail loudly instead of clamping at zero and reporting a plausible number.
  6. Handle carry-in: a hold created before the window contributes its remaining amount as the sweep's initial value, not as a + event inside the window. Omitting that is the off-by-a-day that makes the first minute of every day look artificially quiet.

Follow-up

  • An incremental authorisation raises an existing hold after the fact. Where does that event go, and does it disturb the tie rule?
  • You now need the peak for 10 million accounts inside a nightly window. What changes, and what must the partitioning key be?
  • The peak sizes a funding transfer. Does the business date or the timestamp decide which day that transfer lands on?

Version a loan schedule instead of soft-deleting posted instalments

Medium · audit history · soft deletes · temporal queries

loan_instalment is keyed by (loan_id, schedule_version, instalment_no) and carries due_date, principal_minor, interest_minor, fee_minor, paid_principal_minor, paid_interest_minor, status, days_past_due, effective_from (date) and superseded_at (timestamptz). A borrower defers two payments on 2026-03-14, and instalments 1 to 6 already have allocations posted against them. Model exactly what the deferral writes, and write the query that returns the schedule as the borrower saw it on an arbitrary date. Say why stamping the old rows with deleted_at, or updating them in place, fails an audit.

Approach

  1. Treat the deferral as an insert, not an edit: write a complete new schedule_version with effective_from = the deferral instant on 2026-03-14, and in the same transaction stamp superseded_at on every row of the outgoing version with that identical instant, so the two version windows are half-open and adjacent rather than overlapping. Instalments 1 to 6 are reproduced unchanged, because they are what the borrower was told and what was posted to the ledger.
  2. Write the as-of read as a version selection, not a row filter: WHERE loan_id = $1 AND effective_from <= $2 AND (superseded_at IS NULL OR superseded_at > $2), then assert that exactly one schedule_version comes back - COUNT(DISTINCT schedule_version) = 1, not one row - so an overlapping window raises rather than silently returning two interleaved schedules under one instalment_no.
  3. Fix the type mismatch before writing that predicate: effective_from is declared date and superseded_at timestamptz, so comparing them casts the date at the session TimeZone and two readers in different zones select different versions near midnight. Put both columns in one domain, timestamptz, keep the window half-open with effective_from inclusive and superseded_at exclusive, and resolve a bare as-of date to an instant once, in the loan's booking timezone, at the edge of the system.
  4. Say what deleted_at loses. It records that a row stopped being current but not what replaced it or from when, it leaves every downstream query obliged to remember deleted_at IS NULL, and one query that forgets double-counts the schedule. Versioning puts the same information in the primary key where it cannot be forgotten.
  5. Close the arithmetic: under the loan's stated day-count convention the new version must still sum to outstanding principal plus scheduled interest to the minor unit, with the per-period rounding residual placed in one named instalment, conventionally the last, rather than smeared across the tail.
  6. Index (loan_id, effective_from DESC) for the as-of lookup, keep superseded versions online rather than archiving them, and enforce with a trigger that no UPDATE touches a row whose paid_principal_minor or paid_interest_minor is non-zero.

Worked solution (25 min)

  1. Insert a 12-instalment version 1 with effective_from at origination, then allocate payments against instalments 1 to 6.
  2. Apply the deferral: insert version 2 effective at the 2026-03-14 deferral instant, reproducing instalments 1 to 6 byte for byte and re-amortising 7 to 12, and set superseded_at on all twelve rows of version 1 to that same instant in the same transaction.
  3. Write the as-of query with the version-selection predicate and run it for instants resolved from 2026-03-01 and 2026-03-20 in the loan's booking zone, and for the deferral instant itself.
  4. Compare SUM(principal_minor) per version against the original principal and locate the rounding residual.

Expected result: Two schedule_versions for the loan. The as-of query returns version 1 for 2026-03-01 and version 2 for 2026-03-20, instalments 1 to 6 are identical across both versions, and SUM(principal_minor) equals the original principal exactly in each version.

Verify it yourself

  • The live-version invariant is per version, not per row: SELECT COUNT(DISTINCT schedule_version) FROM loan_instalment WHERE loan_id = $1 AND superseded_at IS NULL returns 1, while COUNT(*) over the same predicate returns that version's instalment count - 12 here - so an assertion of one NULL row per loan would fail on the correct design.
  • Run the as-of query for 200 random dates spanning the loan and assert it returns exactly one schedule_version every time.
  • Run the as-of query at exactly the deferral instant and assert version 2 alone: the windows are half-open and version 1's superseded_at equals version 2's effective_from, so the boundary belongs to exactly one version.
  • Attempt an UPDATE on an instalment with paid_principal_minor > 0 and confirm the trigger rejects it.
  • Change the session TimeZone and re-run the boundary instant; with both columns timestamptz the selected version must not change, which is the test that the date column was actually retyped.

Follow-up

  • What does days_past_due mean for an instalment that exists in two versions with different due_dates?
  • A payment arrives allocated to an instalment_no that exists only in the superseded version. What do you do with it?
  • How do you prove the ledger postings made under version 1 still reconcile once version 2 exists?

Add and backfill business_date on a live ledger table

Hard · online migration · lock queue · backfill

ledger_entry holds 4 billion rows, is append-only, takes 10,000 inserts per second, and every reporting query currently derives the business date as posted_at::date. You must add business_date date NOT NULL, populated from the cutoff rule (17:00 in the account's own timezone), backfilled across all history, indexed, and cut over, with no write downtime and no long-held lock. Give the ordered migration steps with the lock each one takes, how you make the backfill restartable and throttled, and how you retire the old expression safely.

Approach

  1. Add the column nullable and with no default. ALTER TABLE ... ADD COLUMN takes ACCESS EXCLUSIVE but is a catalogue-only change held for microseconds. The hazard is the lock queue, not the statement: a blocked ALTER waits behind one long reader holding ACCESS SHARE, and every query arriving afterwards queues behind the ALTER's pending ACCESS EXCLUSIVE, so set lock_timeout to a couple of seconds and retry rather than letting a metadata change take the table down.
  2. Deploy the write path before the backfill, so new inserts populate business_date from the cutoff rule while reads stay on the old expression. The backfill then chases a closed set with a fixed upper bound instead of a moving target.
  3. Backfill in bounded batches keyed by entry_id range, on the order of 50,000 rows per statement, committing between batches and recording the high-water mark in its own table so a killed run resumes instead of restarting. WHERE business_date IS NULL makes each batch idempotent, and the pacing is set by replica lag and dead-tuple growth rather than CPU, since each UPDATE writes a new row version and the WAL volume is proportional to the rows touched.
  4. Install the constraint without a blocking scan: ALTER TABLE ... ADD CONSTRAINT ck_business_date CHECK (business_date IS NOT NULL) NOT VALID takes a brief ACCESS EXCLUSIVE and scans nothing, then VALIDATE CONSTRAINT takes SHARE UPDATE EXCLUSIVE and runs alongside reads and writes. On PostgreSQL 12 and later, SET NOT NULL can then use the validated CHECK and skip its own full scan; on 11 and earlier it always scans, so the CHECK is the migration on those versions.
  5. Build the index with CREATE INDEX CONCURRENTLY, which avoids ACCESS EXCLUSIVE at the cost of two table passes, cannot run inside a transaction block, and on failure leaves an INVALID index that must be dropped and rebuilt rather than reused.
  6. Cut over behind a flag: run the new and old expressions side by side for one reporting cycle and compare totals per day, since the cutoff rule will legitimately move entries near 17:00 across the boundary. Only once they reconcile do you retire the posted_at::date expression index, and you keep posted_at as the ordering key rather than repurposing it.

Follow-up

  • Reporting now wants ledger_entry partitioned by business_date. Why can this not be another ALTER, and what is the migration instead?
  • Nightly totals move for the days around the cutoff change. How do you tell a correct restatement from a backfill bug?
  • The backfill is halfway done when a replica falls 20 minutes behind. What do you throttle, and what do you refuse to throttle?

A string is a special type of object—can you explain its underlying properties?

Medium

Approach

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

Follow-up

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

Can you explain the concept of a Java linked list?

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. Clarify what is being asked and what a complete answer contains.

Follow-up

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

Decide the risk fallback when the feature store degrades

Medium · graceful degradation · circuit breaker · availability · timeouts

The risk decision service returns approve, decline or refer for one authorisation at 3,000/s within an 80 ms p99, using at most two feature-store round trips; the orchestrator allows it 80 ms inside a 2-second caller timeout. The feature store loses a region and its p99 becomes 900 ms. Design the degraded path: what the risk service returns when its own deadline expires, what the orchestrator does when risk does not answer at all, how the resulting exposure is bounded to a stated number, and how you measure afterwards what the choice cost.

Approach

  1. Answer two separate questions, because they have different owners. Inside risk: what is returned when its own deadline expires. Inside the orchestrator: what happens when risk returns nothing at all. The orchestrator cannot infer the first from silence, since a timeout and a decline are indistinguishable at that boundary, which is precisely why the fallback has to be an explicit decision rather than whatever the catch block does.
  2. Give the risk service a hard internal deadline below its budget, around 60 ms, that cancels the feature fetch outright and falls through to a rules-only decision over inputs it already holds: amount, instrument token age, merchant category, in-process velocity counters. Return the decision with a degraded flag and a reason code so it is auditable later. Never let the cancelled fetch keep running, and never recompute features synchronously as a fallback.
  3. Open a circuit breaker on the feature store after a threshold of deadline expiries. That removes the timeout amplification and returns the service to a rules path measured in single-digit to low tens of milliseconds, well inside budget; a low-rate half-open probe detects recovery. Without the breaker, every request pays the deadline before degrading, and the queue in front of the service does the rest.
  4. Make the product decision explicit as approve-under-cap: approve degraded decisions only below an amount threshold and only for parties with prior settled history; decline or refer above it. Worst-case exposure is then parties in the window multiplied by the cap multiplied by the per-party count cap, which is a number you can put in front of an owner before the incident instead of reconstructing after it. Compare it against the other side of the trade: declines on a degraded segment that is mostly legitimate, priced at approval rate times average value.
  5. Tag every degraded authorisation and, after the dispute window closes, compare that cohort's chargeback rate against the non-degraded baseline. That comparison is the only evidence the cap was set correctly, and without the tag it can never be computed at all.

Worked solution (25 min)

  1. Add a 60 ms internal deadline that cancels the feature fetch, plus a rules-only path, returning the decision with a degraded flag and reason.
  2. Inject 900 ms p99 into the feature store, drive 3,000 decisions/s, and record the approve/decline/refer split and the degraded share.
  3. Open the breaker after a threshold of expiries and re-measure service p99 and orchestrator timeout count.
  4. Apply the approve-under-cap policy and compute worst-case exposure as parties in the window times the amount cap times the per-party count cap.

Expected result: With the deadline and an open breaker, the service answers on the rules path in single-digit to low tens of milliseconds and the orchestrator records no timeouts at all. Without them, p99 exceeds the 80 ms budget and the orchestrator sees timeouts it cannot distinguish from declines. Worst-case exposure comes out as a specific currency figure derived from the three caps, written down before the incident rather than after it.

Verify it yourself

  • Every degraded decision is persisted with its flag, so the cohort's chargeback rate is computable once the dispute window closes.
  • No code path returns approve merely because an exception was caught; read the timeout handler and confirm.
  • The caps and breaker thresholds are runtime configuration, changeable mid-incident without a deploy.

Follow-up

  • The risk service is not slow but entirely unreachable. Does the orchestrator's answer change?
  • How do you keep the degraded flag out of the customer-facing decline reason while keeping it in the audit record?
  • Do you re-score degraded approvals asynchronously afterwards, and what do you do with one that scores badly?

One merchant stops receiving webhooks while the rest deliver

Easy · poison message · head-of-line blocking · outbox

The outbound relay delivers outbox_event rows to merchant endpoints preserving per-destination ordering. One merchant has received nothing since 09:14; every other destination is current. The relay process is healthy and CPU is flat. For that merchant, unpublished rows are accumulating, the oldest has attempts = 412 with backoff capped at 30 seconds, and last_error is the same string on every attempt. Diagnose in order, and state what you change so one undeliverable row can never stall a destination again.

Approach

  1. Separate destination down from one row undeliverable before anything else, because they are identical from the merchant's side. The discriminator is the error class: a transport error (connection reset, 503, TLS failure) varies and implicates the endpoint, while an identical deterministic error on every one of 412 attempts implicates the row.
  2. Read the blocked set in version order: SELECT event_id, aggregate_id, aggregate_version, attempts, last_error FROM outbox_event WHERE published_at IS NULL AND aggregate_id = $1 ORDER BY aggregate_version LIMIT 5. If everything is queued contiguously behind one row, the ordering guarantee is working exactly as designed and the defect is that it has no exit.
  3. Confirm outside the relay: replay that single payload against the destination by hand. Reproducing the identical error proves the row, not the transport, and stops the escalation to the merchant before it is sent.
  4. Decide what skipping means under a per-destination ordering contract. You cannot simply publish the next row; you either park the poison row in a dead-letter table and accept that the consumer sees an aggregate_version gap, or you deliver a degraded payload. Name which, and say what the consumer does with the gap.
  5. Fix the real defect, which is unbounded retry, not the malformed payload. Bound retries by attempt count or row age, park on exhaustion, alert on the age of the oldest unpublished row per destination, and validate the payload at INSERT time so it cannot be written inside the state-change transaction at all.

Follow-up

  • The consumer orders on aggregate_version. What does it do with the gap your dead-letter created, and how does it learn the gap is intentional?
  • How do you distinguish this from a destination returning 4xx on every request during its own deploy?
  • Does the alert go on attempts, on the age of the oldest unpublished row, or both, and what are the thresholds?

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.

What the behavioural round is actually measuring

Every story you tell gets read for blast radius and judgement: what could have broken, who else it touched, what you knew at the moment you decided. Nobody can audit your code in an hour, so they audit your reasoning instead. Pick work where the call was genuinely yours and the consequences were real enough to remember.

What types of applications have you developed, and what made them unique or useful?

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 did you decide not to do, and why?
  • How did you know your change caused the improvement?

Where do you see yourself in five years?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Give the blast radius: what could have broken, and what you measured.
  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?

Tell me about your work experience and the projects you have led.

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

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

What are your experiences or knowledge regarding Gosu or business rules engines?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

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