Interview guides · Goldman Sachs
Goldman Sachs Software Engineer Interview Questions & Guide 2026
Timed coding practice for Goldman Sachs software engineer candidates: full attempts under a clock, then scored against a rubric.
Interviewing at Goldman Sachs
- Practice bank: 20+ questions
- Rounds: 3
- Typical prep: 2–4 weeks
- Interview reports: 1
Interview rounds
| Round | Questions |
|---|---|
| Online Assessment | 2 |
| Technical Screen | 8 |
| Onsite | 10 |
How hard is the Goldman Sachs Software Engineer interview?
| Difficulty | Share | Questions |
|---|---|---|
| Easy | 30% | 6 |
| Medium | 70% | 14 |
| Hard | 0% | 0 |
What Goldman Sachs actually tests for
| Topic | Share | Questions |
|---|---|---|
| Coding & Algorithms | 70% | 14 |
| Behavioral & Leadership | 10% | 2 |
| Software Engineering Fundamentals | 10% | 2 |
| System Design | 10% | 2 |
The 60-second brief
As a Software Engineer at Goldman Sachs, you sit at the direct intersection of modern software engineering and complex global finance. Engineering at Goldman Sachs is not a background support function; it is a core business engine that powers high-frequency trading platforms, sophisticated risk management frameworks, asset management portals, and client-facing digital financial platforms like Marquee. You will build and scale resilient distributed systems capable of processing millions of transactions per second under tight latency constraints and stringent regulatory controls.
Goldman Sachs candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
What a Software Engineer does at Goldman Sachs
In this role, you will collaborate closely with quantitative strategists, traders, portfolio managers, and global product teams to solve complex technical problems. Depending on your team alignment—such as Global Banking & Markets, Asset & Wealth Management, or Core Risk Technology—you will write clean, production-grade code in languages like Java, C++, Python, or. You will be responsible for end-to-end design, implementation, automated testing, and deployment of scalable microservices, low-latency messaging architecture, and data pipelines.
The engineering culture at Goldman Sachs emphasizes technical precision, analytical rigor, and operational excellence. You will face challenges involving high-volume throughput, strict consistency, fault tolerance, and secure identity management. Success in this role requires not only mastery of core computer science fundamentals—such as data structures, algorithms, and system design—but also a proactive ownership mindset and the ability to articulate technical decisions clearly to non-technical stakeholders.
The Goldman Sachs interview process
Goldman Sachs candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screen
The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.
What it evaluates
- Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
- Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
- Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural
How to prepare
- Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
- Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
- Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one
Online Assessment
What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What it evaluates
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Technical Interviews
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 it evaluates
- 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.
Behavioral Interviews
Your first answer is not really what is scored. It buys the follow-up questions, and those decide the round. An interviewer with fifteen minutes takes one thread and pushes on it four or five times, so a story you can only tell at a single level of detail collapses under the third why. That is an argument for fewer stories known deeply rather than one prepared per prompt. Four or five pieces of work you can still explain down to the code you changed and the argument you had about it will cover nearly anything asked in this round.
What it evaluates
- Whether a story holds as the questioning moves from what you did to why that instead of the alternative, and then to what you would change knowing what you know now
- Whether you can re-cut a project to answer the question actually asked rather than delivering a rehearsed block that answers an adjacent one
- Whether your level of detail is chosen rather than habitual: going down to the schema when the question is about the data model, staying out of it when the question is about the person who disagreed with you
How to prepare
- Pick four projects and write the chain out four levels deep for each: what you did, why that, why not the alternative, and what would have to be true for the alternative to have won. Where you cannot reach the fourth level, you have a placeholder rather than a story
- Have someone ask why three times in a row on a single thread with nothing else added, and mark the point where you start repeating a sentence you already said. That point is where the interviewer stops learning anything
- Build a one-page index instead of an answer bank: the common prompts in this round (disagreement, a failure that was yours, thin requirements, a deadline you missed, work you inherited) mapped to which of your four projects you would use for each, so the choosing is done now rather than while an interviewer waits
Questions Goldman Sachs candidates report
Data structures and algorithms
Every fast solution here throws work away, and the claim you have to defend is that the discarded work could not have held the answer. A two-pointer walk over sorted values advances the left index only because every remaining pair using that element is also too small. A monotonic deque drops an element because a larger one sits to its right and will stay in the window longer. Say that sentence out loud. A weak answer names the technique and shows it passing the given examples, which is indistinguishable from a wrong technique that happens to pass the given examples.
- Robot Grid Movement: Given an
N x Ngrid with values, calculate the path that collects the maximum total value moving from(1,1)to(N,N), handling edge cases and dynamic programming states. - Josephus Elimination Game: Solve the circular elimination problem where candidates sit in a circle and every $K$-th candidate is removed until one candidate remains.
- Trapping Rainwater: Calculate how much rainwater can be trapped after raining given an elevation map represented by an array of non-negative integers.
- High Five / Top Marks Calculation: Given a list of student exam scores, write a function to return the top average score for each student using an efficient map and priority queue approach.
System design and architecture
The design is the data model and the interface; the boxes follow from them. Write the entities, the key each row is stored under, and then, for every read the product needs, the path from a key to the rows returned. Do the same for each write, including whatever else must change in the same operation. Most designs fail here rather than at scale, when a read the feature depends on turns out to need a scan or a fan-out across every partition. The strongest answers state the queries first and let the storage choice fall out of them.
- Credit Card Fraud Check Engine: Design a microservice that enforces merchant category code (MCC) rules and dynamic user transaction velocity limits over rolling windows.
- Image Storage & Media Service: Create an architecture for uploading, storing, and serving image assets at scale with low latency and effective dynamic caching.
Behavioural and engineering judgement
Most engineering disagreements worth asking about are not settled by evidence, because both positions are defensible on what is known at the time: a schema shape, an abstraction one of you thinks is premature, fixing versus rewriting. The useful move is to say what would settle it, and what you do when nothing will. A common tiebreaker is which option is cheaper to undo, so you take the reversible one and set a date to look again. Weak answers argue who turned out to be right. Strong ones explain how the call got made while it was still undecidable.
- Tell me about a scenario where you made a critical mistake in code or production. What immediate steps did you take, and what did you learn?
- Describe a situation where you had to learn a completely unfamiliar technology stack or framework on your own to deliver a project.
Other topics
A large share of these are really questions about evidence: why something is slow, or how much of something you would need. Convert the prompt into a number or an observation. For diagnosis, name the measurement that would tell two candidate explanations apart and say what each predicts before you collect it. For sizing, carry the units through and stop at one significant figure. Listing everything that could plausibly be true is what the weak version looks like. What separates the strong one is a hypothesis, the observation that would falsify it, and what the arithmetic already rules out before any new data arrives.
- What is the difference between a pointer and a reference in C++, or value vs. reference passing in Java?
Practice exercises with worked solutions
The prompts above tell you what gets asked.
Store multi-currency amounts and prove each transaction balances
ledger_entry holds entry_id, transaction_id, account_id, direction (debit or credit), amount_minor (bigint, CHECK > 0), currency char(3), source_type, source_id, business_date and posted_at. The service posts in JPY, USD and KWD. Specify the column types for money and the reference table that carries each currency's ISO 4217 minor-unit exponent, and say why that exponent must not be a constant in application code. Then write the query that returns every transaction_id whose entries fail to sum to zero within each currency, ordered by the largest absolute imbalance.
How a strong candidate works it
- State the representation: amount_minor is an integer count of minor units and the scale lives with the currency, not in the column. int4 tops out at 2,147,483,647 minor units, which is about 21.5 million units of a 2-exponent currency, so bigint, and the currency code travels on every row that carries an amount.
- Normalise the scale into currency(code char(3) primary key, exponent smallint, name) and join it only at display and parse boundaries. The exponent is 0 for JPY and KRW, 2 for USD and EUR, 3 for KWD, BHD, JOD, OMR and TND, so a hard-coded divide by 100 is wrong by 100x for JPY and by 10x for KWD, in opposite directions.
- Rule out binary floating point outright: IEEE 754 binary64 cannot represent 0.1, so repeated accrual drifts by a few minor units that later appear as reconciliation breaks. If a fixed-scale decimal is used instead, pin the scale per currency and name the single place rounding happens.
- Write the balance check as a grouped aggregate: sign the amount with a CASE on direction, GROUP BY transaction_id, currency, and HAVING the signed sum <> 0. The sign belongs in direction, and the CASE is the one place it becomes arithmetic.
- Then order in a second layer, because the ordering the question asks for cannot be written on the grouped query itself. PostgreSQL lets an output alias stand alone in ORDER BY but resolves anything inside an expression against the input columns, so ORDER BY abs(imbalance_minor) DESC raises SQLSTATE 42703, column "imbalance_minor" does not exist, while the bare ORDER BY imbalance_minor DESC is accepted and sorts signed rather than absolute. Wrap the grouped query and order by abs() outside it, or repeat the aggregate as ORDER BY abs(SUM(CASE ...)) DESC, which is legal because aggregates are allowed there.
- Group per (transaction_id, currency) rather than per transaction, because a cross-currency movement balances only within each currency, bridged by an explicit FX position account whose rate, source and timestamp are stored on the transaction.
- Note that no CHECK constraint can express this, since it spans rows: enforce it at write time with a DEFERRABLE INITIALLY DEFERRED constraint trigger that fires at commit, or by routing every posting through one function, and keep this query as the independent audit.
Expected result (15 min): Exactly one row: the deliberately unbalanced transaction, with a signed imbalance_minor equal to the amount omitted. The clean capture and both single-currency sides of the FX transfer return nothing.
Check yourself
- The cross-currency transaction produces two groups, one per currency, and neither appears in the output.
- Flipping one entry's direction makes imbalance_minor exactly twice that entry's amount, which confirms the sign convention.
- Inserting an entry with amount_minor = 0 or a negative value is rejected by the CHECK rather than by application code.
- Execute the statement rather than reading it: ordering the grouped query directly by abs(imbalance_minor) fails with 42703, which is why the abs() sits outside the subquery.
Follow-ups
- A transaction has a USD leg and a JPY leg. Which accounts appear, and where does the rounding residual land?
- How would you enforce the zero-sum rule at write time without serialising all postings behind one lock?
- Someone proposes storing a display amount alongside amount_minor. What goes wrong at the first rate change or rounding rule change?
Version a loan schedule instead of soft-deleting posted instalments
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.
How a strong candidate works it
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Follow-ups
- 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?
Match a settlement file to ledger postings under duplicate keys
You have one business date of ledger postings (about 12 million rows: transaction_id, source_id, amount_minor, currency, business_date) and the processor's settlement file (about 12 million lines: settlement_line_id, external_reference, amount_minor, currency, business_date), where source_id carries the external reference. Match one-to-one on (external_reference, amount_minor, currency, business_date). Duplicate keys occur legitimately — the same amount can appear twice. Emit every unmatched item classified ledger_only, file_only or duplicate_match, in linear expected time. Then say what you do when neither side fits in memory.
How a strong candidate works it
- Build the smaller side into
key -> deque of row ids, neverkey -> row id. A duplicate key is data, not corruption; a single-row map drops one of a legitimate pair and the break report then shows afile_onlythat does not exist. - Probe the larger side once, carrying one extra bit per bucket: whether that bucket was ever hit. Pop from the bucket on a match and set the bit. An absent key is a probe-side-only row. A present-but-empty bucket means the probe side holds more copies than the build side — surplus, so
duplicate_match. After the pass, a leftover non-empty bucket that was never hit is build-side-only; one that was hit is build-side surplus, so alsoduplicate_match. - That bit is what makes the classification a function of the per-key counts rather than of which side you happened to build. For a key with L ledger and F file copies: min(L, F) match, and the |L - F| surplus rows are
duplicate_matchtagged with the side that is over, degenerating toledger_onlyorfile_onlyexactly when min(L, F) is 0. Without the bit, surplus is only observable as a present-but-empty bucket, which can only ever happen on the probe side — so the same input reports different break classes depending on build order, and the smaller-side heuristic in bullet one silently decides which. - A genuine amount difference does not surface as
amount_mismatchhere, because the amount is inside the key — it surfaces as aledger_onlyand afile_onlysharing a reference. Promote those in a second, separate pass keyed on reference alone, recording signeddelta_minoras ledger minus file. Keep that promotion out of the exact pass. - Cost: O(N+M) expected time and O(min(N,M)) memory; the hit bit packs into the bucket header and changes neither bound. The constant is the hash map, roughly 60 to 100 bytes per entry in most runtimes, so 12 million rows is order 1 GB — measure it rather than assert it.
- When neither side fits, use a grace hash join: partition both sides with the same hash function into P spill files so a key lands in the same partition on both sides, then join partition by partition in memory. Cost is two extra sequential passes; skew inside one partition is the failure mode, handled by re-partitioning that partition under a second hash.
- Sort-merge is the alternative at O(N log N + M log M) with external sort, and it wins when the file already arrives sorted by reference or the output must be ordered. It also gets the surplus classification for free, since a merge sees L and F side by side. Whichever you pick, do not widen the amount comparison to make breaks disappear: a tolerance wide enough to absorb rounding is wide enough to absorb a real loss.
Expected result (30 min): 9 matched pairs, 2 ledger_only, 1 file_only, 1 duplicate_match — the file's surplus third copy — satisfying 2*9 + 2 + 1 + 1 = 22 = N + M. Building on the file side and probing with the ledger returns those same four counts with the surplus still attributed to the file, because every label is derived from L and F. Without the hit bit it does not: the swapped run reports ledger_only 2, file_only 2, duplicate_match 0 — the identity still sums to 22, and the surplus file copy has been silently reclassified as an absence, which is the failure this fixture exists to catch.
Check yourself
- The counting identity holds on every run, including the skewed one.
- The full break classification — counts and per-row labels — is identical when build and probe sides are swapped. A diff there means a label is being read off the join's mechanics rather than off the data.
- Signed
delta_minoron a promotedamount_mismatchis ledger minus file, and flips sign when the sides are swapped. - Forcing P = 1 in the grace-hash path reproduces the in-memory join's break set exactly, proving partitioning is not changing the answer.
Follow-ups
- The file nets three fee lines into one batch total. Which pass catches that, and what is its stopping rule?
- The processor's business date sits one cutoff behind yours for forty minutes of traffic. What does that do to the exact join, and what does it do to break ages?
- The same break recurs on the next run. Why must it link to the existing
reconciliation_breakrow rather than open a second one?
Specify the idempotency key contract for payment creation
POST /payments takes an idempotency key header plus amount_minor, currency, merchant and instrument token. The caller is a merchant server that retries on timeout and may retry concurrently from two workers. Storage is idempotency_key (id, scope text, key text with UNIQUE (scope, key), request_fingerprint bytea, status enum in_progress/completed/failed, response_status smallint, response_body jsonb, locked_at, completed_at, expires_at). Specify the scope, what the fingerprint covers, the response when the same key arrives with a different body, the response while the first request is still in flight, and what a replay after expires_at does.
How a strong candidate works it
- Scope the key to caller plus operation, for example 'merchant:1234:create_payment'. A global key space lets two merchants collide, and a key scoped only to the merchant lets a key used on one endpoint replay that endpoint's stored response for a different operation.
- Fingerprint the canonicalised body: sorted keys, no insignificant whitespace, amounts as integer minor units. Cover the fields that determine the effect and deliberately exclude client metadata such as a trace id, so a retry that adds one is still recognised as a retry. Same key with a different fingerprint is 409 with its own code, never the cached response and never the new effect.
- Win the concurrent race in the database: INSERT ... ON CONFLICT DO NOTHING RETURNING id. An empty return means this request lost, so it reads the winner's row. A SELECT-then-INSERT is not a fix; under READ COMMITTED both workers take fresh snapshots and both pass the check.
- Define in_progress explicitly. The loser reads status in_progress and receives 409 with a retry hint rather than a blocking wait or a second attempt. Note that Retry-After is defined for 429, 503 and 3xx, so on a 409 it is an extension you document rather than something the caller's HTTP stack already honours. A locked_at older than the lease is reclaimable only through a conditional UPDATE that matches the old locked_at, otherwise two reclaimers both proceed.
- Commit the key row before calling the processor, so a crash leaves evidence of an attempt that can be queried and converged. On completion store response_status and response_body and replay them verbatim, including the original status code.
- State the retention window (24 hours is a common choice) and its consequence: after expires_at the same key is a new request, so the caller's total retry deadline must sit inside the window or the mechanism silently stops protecting anything. The header name comes from an IETF draft rather than a ratified RFC, so document it as your convention.
Expected result (30 min): One payment row and one processor call for the 50 concurrent requests, identical replayed responses including status code, 409 with a distinct code for the changed body, and convergence to a single charge after the injected crash.
Check yourself
- An instrumented processor client records exactly one outbound call across the 50 concurrent requests.
- The losing inserts never execute the effect; they only read the winner's stored response.
- The post-expiry replay behaviour is asserted by a test rather than left to whatever the code happens to do.
Follow-ups
- The processor call succeeds but your commit fails. What does the next request with the same key see, and what converges the row?
- A merchant reuses one key for two genuinely different payments a month apart. Which of your rules catches it, and which lets it through?
- Where does the key live end to end, and what changes if the downstream processor honours its own key?
Authorisation p99 tripled overnight with no deploy
Payment orchestration serves about 3,000 authorisations per second against a 150 ms p99 budget. Since 02:00, p99 is 460 ms and rising about 8 ms per hour, while p50 is unchanged at 11 ms. There was no deploy, no traffic change and no processor degradation. The hot path does one INSERT into idempotency_key, which has UNIQUE (scope, key), then two UPDATEs on that row: locked_at, then status and response_body. A nightly reconciliation job started at 01:50 and is still running. Give the ordered diagnostic checklist and the cause.
How a strong candidate works it
- Read the shape first. p50 flat with p99 rising and no deploy is a resource or data-volume effect, not a code path, because a code change moves the median too. A tail that climbs monotonically at fixed workload means something monotonically grows.
- Ask what started at 01:50. In PostgreSQL an open transaction holds back the xmin horizon cluster-wide, so autovacuum can reclaim no dead tuple newer than that snapshot. Confirm with pg_stat_activity (state, now() - xact_start, backend_xmin) and with pg_stat_all_tables (n_dead_tup, last_autovacuum) for idempotency_key.
- Connect it to the write pattern. Three writes per key produce up to two dead tuples each, so at 3,000 rps the table sheds roughly 6,000 dead tuples per second. Heap-only tuple updates would keep those out of the index, but only when no indexed column changes and the page has room, and appending response_body grows the tuple enough to force a new page. So the unique index on (scope, key) grows too.
- Explain why only the tail suffers. A larger index means more pages per lookup and a rising fraction of them missing shared_buffers; the median request still hits cache while the tail pays physical I/O. This is exactly the p50-flat, p99-rising signature, and it is worth stating before acting.
- Verify before fixing rather than after. n_dead_tup in the millions and rising, last_autovacuum stale since about 01:50, and pg_relation_size on the unique index measured twice fifteen minutes apart showing growth at constant workload. Index bloat cannot be inferred from row count alone; use the size series or pgstattuple.
- Fix in two moves and prevent separately. End or chunk the long transaction so the reconciliation job commits per batch instead of holding one snapshot over 50M lines, then let autovacuum catch up or run REINDEX CONCURRENTLY. Add a transaction-age alert and a statement timeout on the reporting role. Collapsing the two UPDATEs into one and lowering fillfactor halves dead-tuple production, but that is an optimisation, not the cause.
Follow-ups
- The reconciliation job legitimately needs a consistent view of 50M lines. How do you give it one without pinning the xmin horizon?
- Why did p50 not move at all?
- You also run an expires_at cleanup job that DELETEs old idempotency keys. During this incident, does running it help or hurt, and what design avoids the question entirely?
Ship payouts with named, scheduled reconciliation debt
You have six working days to ship merchant payouts. The correct reconciliation join is on (external_reference, amount_minor, currency, business_date); the version that fits the window matches on amount and business_date alone, which will mismatch same-amount, same-day lines. Describe a time you shipped with debt you knew about. State exactly what you cut, the guard that made the shortcut visible rather than silent, who agreed to carry the risk, the date it was paid down, and what it cost while it stood. Be specific about the failure you were accepting.
How a strong candidate works it
- The probe is whether your shortcuts are bounded and instrumented or merely undocumented. Start by stating the failure you accepted in one concrete sentence: ambiguous matches will be resolved arbitrarily, so a genuine break can be masked by pairing with the wrong line.
- Make the shortcut loud rather than silent. When more than one candidate matches, do not pick one: open a break of type duplicate_match and emit a counter on that path. A shortcut that reports its own frequency converts an unknown into a number you can bring to the paydown conversation.
- Bound the exposure before you ship: query historical settlement files for lines that collide on (amount_minor, business_date) within a merchant. If that is 0.2 percent of lines, the risk is small and quantified; if it is 8 percent, the plan does not survive contact and you have learned that before shipping rather than after.
- Name the owner and the date, and say where they are written down. 'The team agreed' is the generic answer; a ticket with an owner, a date, and the counter that forces the conversation is the strong one.
- Distinguish debt you can pay from debt that compounds. A matching shortcut that leaves breaks visible is recoverable; one that auto-resolves breaks by widening a tolerance destroys the evidence and cannot be unwound later, and a strong answer says it would have refused that version even under the deadline.
- Report the actual cost: how many ambiguous matches occurred, how much analyst time they consumed, and whether the paydown date held. If it slipped, say by how long and what finally forced it.
Follow-ups
- The paydown date arrives and the team has a new deadline. What do you do differently from the first conversation?
- Which shortcut would you have refused to ship at any deadline, and why that one?
- How would you have sized the exposure if no historical settlement files were available?
Where candidates lose points
Assuming the default isolation level enforces the invariant you wrote down
PostgreSQL defaults to READ COMMITTED, where every statement takes a fresh snapshot, so a read-modify-write on a balance loses updates under concurrency. Its REPEATABLE READ is snapshot isolation, which blocks that particular anomaly by aborting the loser with SQLSTATE 40001 but still permits write skew across two different rows; only SERIALIZABLE closes that, and both levels therefore require a bounded retry loop on 40001 that many implementations simply never write. MySQL's InnoDB REPEATABLE READ behaves differently again — it does not abort on a conflicting write, so the identical application code silently changes behaviour when the engine changes. Two-sided transfers add a second failure mode on top: without a deterministic lock ordering, such as always locking account ids in ascending order, concurrent opposing transfers deadlock (SQLSTATE 40P01).
Writing the state change to the database and publishing the event in the same code path
No transaction spans a relational database and a message broker, so a crash between the two leaves one done and the other not, and the failure is asymmetric in both orderings: publish-then-commit invents events for state that never existed, while commit-then-publish silently loses events for state that does. Retrying the publish after the commit is not a fix, because the process can die before the retry runs. The working shape is an outbox row written inside the same transaction plus a relay that publishes it at least once, which makes consumer-side idempotency mandatory rather than optional. Note also that 'exactly-once' in a stream processor means exactly-once processing within that system's own read-process-write transaction, and says nothing at all about an external side effect such as charging a card.
A cache with no invalidation story
Say how an entry goes stale, how long you can serve it stale, and what happens when many requests miss the same key at the same instant. One popular key expiring under load sends every concurrent request to the origin together; single-flight coalescing, jittered expiry, or serving stale while revalidating are the standard answers.
Retrying a write that is not safe to repeat
A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.
Algorithms first, for a candidate whose problem-solving has gone rusty
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.