cswork / libraryOpen practice workspace

Interview guides · Beyond Finance

Beyond Finance Software Engineer Interview Guide

Beyond Finance Software Engineer interview guide: code review exercises, SQL optimization, legacy code, production debugging and a 7-day prep plan.

The 60-second brief

Beyond Finance is described as a financial technology company. Candidates describe the Software Engineer role as building and maintaining the systems behind products that serve the company's clients, working with product managers and other cross-functional teams to turn business requirements into software, and working with production support teams on reliability. Feature work is reported to share the calendar with technical debt reduction. No specific products, programming languages, frameworks or team names have been reported, so this guide prepares you for the skills the reported questions test rather than for a particular stack.

This guide is for candidates interviewing for a Software Engineer role at Beyond Finance. It covers the reported themes: explaining and refactoring a code snippet, modernizing legacy code, SQL optimization on large data, debugging a production issue with an unclear cause, and keeping code maintainable when releases move quickly. It also covers the project stories and collaboration questions candidates report for later conversations. It does not cover team-specific details, because none have been reported.

Candidates report a series of conversations that runs from a recruiter screen to panel interviews with engineering managers and cross-functional partners such as product managers, with code review exercises and SQL work reported in place of standard live coding.

Focus areas

  • SQL Fundamentals
  • Technical Interview (Code/Design Discussion)
  • SQL Query Writing / Assessment Handling

Overview

The reported topics cluster tightly: SQL fundamentals, SQL query writing and assessment handling, a technical interview that mixes code and design discussion, reading existing code, and a code review exercise. The five reported questions follow the same pattern. Explain a snippet and refactor it for performance, work out why a piece of legacy code looks the way it does and modernize it, optimize a given SQL query for large datasets, debug a production issue whose cause is unclear, and describe how you keep code maintainable during fast releases. Almost nothing here is a from-scratch algorithm puzzle, so preparation looks different from a typical coding loop.

The skill to build is reading code you did not write and talking about it in a structured order. Start with what the code does, its inputs, outputs and side effects. Then name what is wrong or risky: correctness hazards first, then clarity, then performance. Say what you would measure before touching anything for speed, and how you would prove the refactor did not change behavior. For legacy code, give a plausible reason for each odd choice (an old constraint, a hidden caller, a workaround) before you propose replacing it, and describe tests that pin current behavior first.

For SQL, candidates report joins, aggregations and performance optimization as the core. Be able to state the grain of any query you write, spot join fan-out that inflates sums, read an execution plan, and explain why an index is or is not used. Candidates also report that discussing past projects in terms of the reason behind each technical decision works well, and the role is described as working closely with product managers, so have two or three project stories where you can say what the user or business needed and which alternative you rejected.

Where to focus your preparation

The only structure candidates report is a series of conversations from a recruiter screen to panel interviews, so this guide treats the loop as one preparation focus rather than a fixed sequence of stages.

Preparation focus

Candidates report conversations that begin with a recruiter screen and move to panel-based interviews, with engineering managers and cross-functional partners such as product managers taking part. They also report code review exercises used in place of standard live coding, and SQL assessments. No stage-by-stage order or per-stage format has been reported, so confirm with your recruiter which stages involve reading code, writing SQL or discussing past projects, and prepare each skill accordingly. The reported questions cover explaining and refactoring a snippet, modernizing legacy code, SQL optimization, production debugging and code quality. Practise them aloud against code you have not seen before, because narrating your reading of unfamiliar code is the skill a code review exercise exercises.

What to demonstrate

  • Reading an unfamiliar function and explaining what it does and where it is fragile
  • SQL fundamentals: joins, aggregations and query performance
  • Explaining the reasoning behind technical decisions, not only the steps taken
  • Communicating with engineering managers and cross-functional partners such as product managers

How to prepare

  • Explain three unfamiliar functions aloud in a fixed order: purpose, inputs and outputs, side effects, risks, then changes
  • Take one slow SQL query, read its execution plan and fix it one change at a time, checking the result stays identical
  • Write a short debugging runbook and a code quality checklist you can recite for the production issue and fast release questions
  • Ask your recruiter which stages involve code review, SQL work or project discussion, and whether you will write code

Where candidates lose points

Rewriting the snippet before saying what it does

Open every code review answer with a plain summary of purpose, inputs, outputs and side effects. Only then list problems, ordered as correctness, readability, performance, so the refactor answers a diagnosed problem.

Calling a change a performance fix without a measurement or an equivalence check

Name what you would measure first (a profile for code, an execution plan for SQL) and how you would show output is unchanged, for example comparing row counts and checksums between the old and new query.

Tuning SQL by habit: adding an index or DISTINCT without reading the plan

Check the grain, look for one-to-many joins that inflate aggregates, find functions wrapped around indexed columns, and compare estimated rows with actual rows in the plan. A DISTINCT that hides duplicates is a correctness bug, not a speedup.

Dismissing legacy code as bad instead of explaining why it exists

Offer two or three reasons the code may look this way, say how you would confirm each (history, callers, tests), and plan characterization tests before small, reversible refactors.

Describing projects only as what you built, with no reason for the choices

For each story prepare the requirement, the alternative you rejected, the trade-off you accepted and what happened. Be equally direct about tools you have not used, then say how you would get productive in them.

Questions & practice

Archive a resource graph without breaking live references or recursing

Medium · graph traversal · topological order · tenant isolation

Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.

Approach

  1. Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
  2. Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
  3. Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
  4. Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.

Follow-up

  • The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
  • The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?

Canonicalise a request body into a stable idempotency fingerprint

Medium · parsing · canonicalisation · hashing

idempotency_key.request_fingerprint is a SHA-256 over the method, path and canonicalised body, and a retry whose fingerprint differs must be rejected with 422 rather than served the stored response. Write the canonicaliser. Bodies are JSON up to 256 KB nested at most 32 levels; clients vary key order, whitespace and unicode escaping, and some send 64-bit ids as JSON numbers. Produce a deterministic byte string such that semantically identical bodies match and any semantic difference does not. State your complexity and name two normalisations you refuse to perform.

Approach

  1. Parse once into a tree, then re-serialise under fixed rules: object keys sorted, array order preserved, one escaping convention, no insignificant whitespace. Parsing is O(n) and sorting keys is O(k log k) per object, so O(n log n) overall with O(depth) stack, and the 32-level cap is enforced during parsing because hostile nesting is how a canonicaliser becomes a stack overflow.
  2. Sort keys by their UTF-8 bytes and say why the obvious implementation is wrong in some runtimes: a default string comparison that orders by UTF-16 code units places surrogate pairs, meaning code points from U+10000 up, below U+E000 to U+FFFF, which is not UTF-8 byte order, so two services written in different languages disagree on the same document.
  3. Do not re-encode numbers through a double. IEEE-754 binary64 represents integers exactly only up to 2^53, so normalising a 19-digit id through a float changes it, and 1 against 1.0 cannot be reconciled without deciding whether they are the same value. Preserve the literal token, and require ids as strings at the API boundary if you want them comparable.
  4. Reject duplicate keys rather than picking one. JSON permits them and parsers disagree, most keeping the last, so any choice you make ties the fingerprint to a parser detail that the code handling the request does not necessarily share.

Follow-up

  • A client sends the same logical request with an extra field your API ignores. Same key, different fingerprint, so you return 422. Is that the right answer?
  • Where does the fingerprint get computed relative to request decompression and the body-size limit?

Collapse a redelivered event batch into per-aggregate high-water marks

Easy · hashing · at-least-once · aggregation

You drain a batch of up to 5,000,000 events, each (aggregate_id BIGINT, aggregate_version INT, event_type, payload). The log guarantees order within one aggregate only; the batch merges 64 partitions, and a relay failover has redelivered a range, so an older version for an aggregate can appear after a newer one. Given a map of last_applied_version per aggregate, produce the events worth applying, at most one per (aggregate_id, version), plus the count discarded. Target O(n) time. State the memory for 2,000,000 distinct aggregates and what you do when it does not fit.

Approach

  1. One pass, one hash map from aggregate_id to the highest version kept, and a discard counter. An event whose version is at or below last_applied_version for its aggregate is dropped without further work, which is the whole reason the event carries its version rather than a delta. O(n) expected time, O(d) space in distinct aggregates.
  2. Keep the maximum, never the last occurrence. The redelivered range means the final appearance of an aggregate in the batch can be an older version than one seen earlier in the same batch, so last-wins applies stale state over newer state and the projection regresses with no error anywhere.
  3. Cost the memory instead of calling it large: an 8-byte key plus a 4-byte version is 12 bytes of payload, and an open-addressed table held at a 0.7 load factor costs roughly 17 bytes per entry before per-slot metadata, so 2,000,000 aggregates is tens of megabytes in a native layout and several times that in a runtime that boxes both key and value.
  4. If the distinct set exceeds memory, partition on hash(aggregate_id) mod P and reduce each partition independently. Every event for one aggregate hashes to the same partition, so the per-partition result is exact and the merge is concatenation rather than a second reduction.

Follow-up

  • The payload is a patch rather than a snapshot, so applying only the highest version loses the intermediate changes. What changes in your reduction?
  • How do you detect that version 7 arrived while version 6 was never delivered, and what should the consumer do about the gap?

Given a specific SQL query, how would you optimize it to improve execution time on large datasets?

Medium · Technical & Code Review

Approach

  1. Start from the actual plan. In Postgres, run EXPLAIN (ANALYZE, BUFFERS) and look for sequential scans on large tables, estimated row counts far from the actual counts (stale statistics, fixed with ANALYZE), sorts or hashes spilling to disk, and nested loops over large inputs. EXPLAIN ANALYZE executes the statement, so wrap any DML in a transaction and roll it back.
  2. Index the filter and join columns. In a composite index, put equality columns before range columns. A covering index (INCLUDE in Postgres 11+) allows an index-only scan. Then remove predicates that cannot use an index: a function on the column, such as DATE(created_at) = ..., which you rewrite as a half-open range; a leading-wildcard LIKE; or a type mismatch that forces a cast.
  3. Reduce rows early. Select only the columns you need instead of SELECT *. Filter before joining. Aggregate the many side before joining it to avoid fan-out. Replace a correlated subquery with a join or EXISTS. Treat a DISTINCT that only hides duplicates from a bad join as a bug, not a fix.
  4. For very large tables, use keyset pagination (WHERE id > last_id ORDER BY id LIMIT n) instead of a large OFFSET, which still reads every skipped row. Consider range partitioning on a date column when queries filter by date, or a materialized view for an expensive aggregate that can tolerate some staleness.
  5. State the trade-offs. Every index slows inserts and updates and uses storage, and a materialized view needs a refresh strategy. Confirm each gain by re-running the plan and timing it on data of production size.

Follow-up

  • An index exists on the filtered column but the planner still scans the whole table. What are the possible reasons?
  • How would you change this query if the table grew tenfold next year?
  • When would you choose a materialized view or summary table over another index?

Explain why the owner filter ignores the listing index

Medium · composite index · query plans · sort elimination

The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.

Approach

  1. Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
  2. For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
  3. Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
  4. Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.

Follow-up

  • 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
  • A dashboard runs this for 40 owners in one page load. What changes about the design?

Walk me through this snippet of code: what does it do, and how would you refactor it for better performance?

Medium · Technical & Code Review

Approach

  1. Before judging the snippet, explain what it does: its inputs, its return value and any side effects. Trace a small example by hand, and say what happens on empty, null or duplicate input.
  2. Find where the cost is. Look for a membership test on a list inside a loop, which is O(n*m) and drops to O(n+m) with a set because set lookups are O(1) on average. Also look for a query or HTTP call inside a loop (the N+1 pattern), and for a string built with + in a loop, which can be quadratic where join is linear.
  3. Measure before you change anything. Name the profiler you would use (cProfile, a flame graph, or EXPLAIN when the slow part is a query) and the input size at which the cost matters. A loop over 50 items rarely needs fixing.
  4. Refactor in small steps that keep behaviour the same, with a test that pins the current output first. Check what the new structure might drop: a set loses order and duplicates, so confirm callers do not depend on either.
  5. Finish by weighing readability against speed. Say which change gives the largest gain for the least risk, and which change you would skip because it makes the code harder to read for little benefit.

Follow-up

  • How would you prove that your refactor did not change the output?
  • What changes if the input no longer fits in memory?
  • If you could ship only one of your changes, which one and why?

Here is a piece of legacy code: why do you think it was written this way, and what changes would you implement

Medium · Technical & Code Review

Here is a piece of legacy code: why do you think it was written this way, and what changes would you implement to modernize it?

Approach

  1. Assume there was a reason before you remove anything. It may have been an older language or library version, a workaround for a past bug, or a performance constraint. Say you would check git blame, the commit messages and any linked ticket before you call it wrong.
  2. Write characterization tests that pin the current behaviour, odd edge cases included, because callers may rely on a quirk. Only after that do you change the structure.
  3. Name concrete modernizations. Extract long functions. Replace manual open/close with context managers or try-with-resources. Replace SQL built by string concatenation with bind parameters, which also closes an injection hole. Replace magic numbers with named constants, and add type hints.
  4. Rank the changes by risk and value. Security and correctness fixes go first: injection, swallowed exceptions, and floating-point arithmetic on currency amounts, which should use a decimal type or integer cents. Cosmetic rewrites go last or are left out.
  5. For risky changes, roll out gradually. Use a strangler approach or a feature flag, run the old and new paths side by side and compare their outputs, then delete the old path.

Follow-up

  • Which part of this code would you leave untouched, and why?
  • How would you change a function's signature without breaking its existing callers?
  • The original author has left and there are no tests. Where do you start?

Explain your process for ensuring code quality and maintainability in a fast-paced release cycle.

Medium · Technical & Code Review

Approach

  1. Automate the cheap checks so reviews go to design and correctness. Run a formatter, a linter, type checking and unit tests in CI on every pull request, and block merges on failure.
  2. Keep changes small and reviewable. Use short-lived branches or trunk-based development, and use feature flags to separate deploying code from releasing a feature, so unfinished work can merge safely.
  3. Shape the tests as a pyramid: many fast unit tests, fewer integration tests at the database and API boundaries, and a few end-to-end tests on the most important user paths. Aim coverage at the code that moves money or changes data, not at a percentage target.
  4. Make technical debt visible. Log it as tickets with a stated cost, reserve some capacity each cycle to pay it down, and include tests, documentation and monitoring in the definition of done.
  5. Anchor the answer in one real example in the first person. Describe a time you shipped with known debt to meet a date, what you recorded about it, and when and how you paid it back.

Follow-up

  • When would you accept lower quality to ship on time, and how do you limit the damage?
  • How would you show that quality is actually improving? Consider change failure rate, escaped defects and time to restore service.
  • A reviewer blocks your pull request over style while the release is waiting. What do you do?

How do you approach debugging a production issue when the root cause is not immediately apparent?

Medium · Technical & Code Review

Approach

  1. Contain the damage before finding the cause. Assess who is affected and how badly. If users are hurt, roll back the release or turn off the feature flag first, and investigate after.
  2. Build a timeline and a scope. When did it start? Did it coincide with a deploy, a config change, a dependency upgrade, a traffic shift or a data change? Does it hit all requests or a subset: one endpoint, one customer type, one host, one region?
  3. Use the telemetry you have: logs joined by a request or correlation id, error-rate and latency percentiles (p95/p99, not just averages), and traces across service calls. Check that the instrumentation itself is not what broke, for example a dashboard missing a host or a dropped log level.
  4. List hypotheses and test the cheapest one to rule out first. Reproduce the failure with production-like data in a staging environment, and narrow it down with git bisect across commits or by isolating one component at a time. For intermittent failures, look at concurrency causes such as race conditions, connection-pool exhaustion, timeouts and retries.
  5. Close the loop: fix the root cause rather than the symptom, add a regression test, add an alert that would have caught it sooner, and write a blameless postmortem stating the trigger, the underlying cause and the follow-up actions.

Follow-up

  • You cannot reproduce it outside production. What do you do next?
  • How do you decide between rolling back and shipping a hotfix?
  • How would you tell whether the deploy that coincided with the incident is the cause or a coincidence?

Suggested practice checklist

Seven days built on the reported emphasis: reading and refactoring code, SQL query writing and optimization, production debugging, code quality and project stories. Days 2 to 5 each pair a skill with the matching reported questions, and days 6 and 7 rehearse everything together.

Day 1 — Confirm the format and pick your material

  • Message your recruiter to ask which stages are a code review exercise, a SQL assessment, a panel or a behavioral conversation, and whether you will write code or only read it
  • Choose the language you will use for any code you write or discuss
  • Write a one-page inventory of two past projects, each with a decision you made and the alternative you rejected

Deliverable: A sent recruiter message, a chosen language, and a one-page project inventory listing each decision and its rejected alternative.

Day 2 — SQL query writing: joins and aggregations

  • Write queries over a parent and child table: count per parent, sum per parent, and parents with no children, checking each against a hand count
  • Reproduce the fan-out bug by joining two one-to-many tables and summing, then fix it by pre-aggregating each side in a subquery
  • State the grain of every query aloud before you write it

Deliverable: Three saved queries, each with its stated grain, plus a short note on the fan-out bug and its fix.

Day 3 — SQL optimization on large tables

  • Read the execution plan of one slow query and note the scan type, estimated versus actual rows and the costliest node
  • Fix it one change at a time (index, removing a function from an indexed column, selecting fewer columns, pre-aggregating) and re-measure after each
  • Work the practice-set question on why an owner filter ignores the listing index, then say how the answer changes if the table grows tenfold

Deliverable: A before-and-after plan for one query listing each change in order, its measured effect and the check that results are identical.

Day 4 — Read, explain and refactor code

  • Pick three unfamiliar functions and explain each aloud: purpose, inputs, outputs, side effects and failure modes
  • For each, list what you would measure before optimizing and what you would change if the measurement confirmed a problem
  • For one legacy-looking function, write two hypotheses for why it was written that way and a plan: tests that pin behavior first, then small refactors
  • Read the practice-set coding drills as code review material: say what each solution assumes and where it could break

Deliverable: Three spoken explanations and a written characterization-test-first refactor plan for one legacy function.

Day 5 — Production debugging and code quality

  • Write a debugging runbook outline: confirm impact, find what changed, narrow with logs and metrics, mitigate, fix, add a regression test, follow up
  • Rehearse it aloud on an invented incident, such as intermittent errors after a release, naming what each step rules out
  • Write your code quality process by stage (before merge, at release, after release) and mark what you would keep if the schedule shrank

Deliverable: A one-page debugging runbook and a quality checklist that assigns each safeguard to a stage, both rehearsed aloud once.

Day 6 — Project stories and collaboration

  • Write four stories from your own work: a technical decision with its reasoning, a requirement clarified with a product manager, a code review disagreement, and a production issue
  • Give each story a result you can state and one thing you would do differently
  • Practise the practice-set behavioral prompts on review disagreements, unblocking a teammate and defending an estimate

Deliverable: Four stories written as requirement, alternative rejected, trade-off, result, each spoken through once.

Day 7 — Full dry run

  • Run a mixed session: explain an unfamiliar snippet aloud, optimize a SQL query, then answer the debugging and code quality questions back to back
  • Write two questions for an engineering manager and a product manager about how code review and releases work on their team
  • Redo the day whose answer was weakest

Deliverable: A recorded or written mock run with a list of gaps and two questions to ask.

Behavioural preparation

Candidates report that later conversations include behavioral assessment alongside the technical discussion, and that explaining the reasoning behind past technical decisions works well. The role is described as collaborating with product managers and cross-functional teams, owning features through their lifecycle and balancing feature work with technical debt. The prompts below are rehearsal prompts built around those themes, not reported verbatim.

  1. Walk me through a past project and explain why you made each major technical decision, not only how you built it.
  2. Tell me about a time you turned an unclear business requirement from a product manager into an engineering plan.
  3. Describe a code review where you and the author disagreed. What did you write, and how was it settled?
  4. Tell me about a production issue you worked on with a support or operations team. What was your part in the fix?
  5. Describe a time you chose technical debt reduction over a new feature, or the reverse, and how you made the case.
  6. Tell me about a time you had to work with a tool you had never used. How did you get productive?

Turn a code review disagreement into a decision

Easy · code review · optimistic concurrency · communication

A colleague's change updates a row with UPDATE resource SET version = version + 1 WHERE resource_id = $1 AND version = $2 and treats an affected-row count of zero as a successful no-op. You read that as a silently lost update; they think returning 200 is friendlier to clients than returning a conflict. Describe how you have handled a review disagreement of this shape: what goes in the comment, when you leave the thread, and who decides. Then write the comment you would leave here, in under 80 words.

Approach

  1. Sort the disagreement before writing anything. A silently discarded write is a correctness claim about data; the choice between 409 and 412 is taste. Only the first justifies blocking a merge, and saying which one you are doing is most of the value of the comment.
  2. Make the claim reproducible in the comment itself with an interleaving rather than a principle: A reads version 7, B reads version 7, B commits version 8, A's predicate matches zero rows, A is told it succeeded and A's edit is gone.
  3. Offer the alternative with its cost attached: return 409 carrying the current version and the revision that won, so the client can re-read and re-apply. Note that automatic retry is not the fix, because a retry re-reads the winner's state and reapplies an intent formed against data that no longer exists.
  4. Apply an escalation rule you can state: two round trips on the thread, then a call, and the service's owner decides rather than the reviewer. A reviewer who cannot be overruled is a bottleneck with extra steps.

Follow-up

  • Where would you put the test that fails if someone reintroduces the swallowed zero rowcount?
  • The author says clients cannot handle a 409. How do you check whether that is true?

Unblock an engineer without taking the keyboard

Easy · mentoring · leases · at-least-once

A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.

Approach

  1. Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
  2. Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
  3. Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
  4. Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.

Follow-up

  • How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
  • Their handler calls an external endpoint before recording that it did. What do you tell them to change first?

Estimate work you have never done and defend the range

Hard · estimation · backfills · expand-contract

You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.

Approach

  1. Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
  2. Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
  3. Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
  4. Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.

Follow-up

  • How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
  • Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?