Interview guides · Onemain Financial
Onemain Financial Software Engineer Interview Guide
Onemain Financial Software Engineer interview guide: recruiter screen, technical assessment and panel, with BST, BFS, SQL and design practice.
The 60-second brief
Onemain Financial is a lending business. Source notes for this role describe software engineers who build and maintain the backend services and data-driven applications behind its financial products, modernise legacy systems, and ship new features in cross-functional teams. The listed must-haves are proficiency in Java, Kotlin or Node.js, plus SQL and database design and RESTful APIs. AWS, GraphQL and React are listed as nice-to-haves. Use that list as a checklist. Of the reported questions, the API endpoint, live SQL and ActiveRecord, and financial schema trade-off questions map directly onto it.
This guide covers the three stages candidates report: a recruiter screen, a technical assessment or phone interview, and an onsite or virtual panel. It groups the reported questions by category: tree and graph coding, two-pointer string work, API endpoint design, live SQL and ActiveRecord, financial schema trade-offs, high availability and observability, and behavioral. It adds original drills on ledger balances, isolation levels, loan-schedule versioning and webhook delivery. Three drills have worked solutions: deriving per-account balances and catching unbalanced transactions, deciding whether a balance is derived or materialised, and raising the write ceiling on one hot clearing account. A seven-day plan ties each day to a stage or question category.
Focus areas
- Reconcile the ledger against processor settlement files
- Consume webhooks duplicated, delayed and out of order
- Name the isolation level each invariant requires
Overview
Source notes for the Software Engineer role at Onemain Financial describe work on the software behind its lending products: designing, building and maintaining backend services and data-driven applications, taking part in code reviews, refining requirements with product managers, and modernising legacy systems alongside new feature work. The stack named in those notes is Java, Kotlin or Node.js on the server, SQL and database design, and RESTful APIs, with AWS, GraphQL and React listed as nice-to-haves.
The reported questions follow that stack. The coding questions are validating a binary search tree, explaining the two-pointer approach to a palindrome problem and its time complexity, and implementing breadth-first search in a given scenario. Next to those sit a question on best practices for specifying API endpoints and a live coding session in SQL and ActiveRecord. The reported design questions are architecting an application domain from scratch, the trade-offs between database schemas for a financial application, and keeping a distributed system highly available and observable. The question bank for this company and role adds explaining Java in depth, OOP classes in practice, ILE programs and compilation, an IAM, Lambda and database architecture question, lowest common ancestor, first unique character, and designing a chess game.
Candidates report three stages over roughly three to five weeks: a recruiter screen about background and fit, a technical assessment or phone interview, and an onsite or virtual panel with managers and senior engineers that covers past work and professional goals. The sources do not say which reported question comes up at which stage, so prepare every category before the technical assessment. Then use the time before the panel to deepen your project stories and your design answers.
The Onemain Financial interview process
Onemain Financial candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screen
Candidates describe this as an initial call with a recruiter about your background and fit for the role. Treat it as two jobs. First, connect your experience to the stack the posting names (Java, Kotlin or Node.js, SQL and database design, REST APIs) in a two-minute walkthrough. Second, get the details you need for the next stage: whether the technical step is an assessment or a phone interview, which languages you may use, and whether SQL or ActiveRecord is part of it. Raise hard constraints now, such as start date, work authorisation, location and a competing timeline. They are cheap to settle here and expensive to discover at offer stage.
What to demonstrate
- Whether your background walkthrough maps clearly onto the languages, database work and API experience the posting lists
- Whether your constraints on start date, location, authorisation and timeline fit the role before later stages are booked
- Whether you can give a compensation range backed by current data points for the level and location instead of deflecting twice
How to prepare
- Write a walkthrough that names one project per must-have area: a service in your main language, a schema or query you owned, and an API you designed or changed
- Prepare three questions about the next stage: assessment or phone format, allowed languages, and whether SQL or ActiveRecord is in scope
- Write each hard constraint down in one line before the call and state it as a fact, together with the status of any other process you are in
Technical Assessment
Candidates describe this stage as a technical assessment or a phone interview that evaluates technical skills. The sources do not say which reported questions appear here. Prepare the whole reported technical set: BST validation, a two-pointer palindrome with its complexity, BFS in a scenario, API endpoint practices, and live SQL with ActiveRecord. The format changes what matters. On a phone interview, your spoken reasoning is the only evidence of work in progress, so narrate constraints, the brute force and the improvement before you type. In a self-paced assessment, the examples in the prompt are the specification, so test the empty and single-element inputs yourself before you submit.
What to demonstrate
- Whether your code is correct on edge cases you name yourself: an empty tree, duplicate keys, a disconnected graph, a single-character string
- Whether you state time and space complexity and can justify them; the palindrome question asks for time complexity directly
- Whether you can move between an ORM call and the SQL it produces when a data-access question comes up
How to prepare
- Solve BST validation with bounds passed down the recursion and again with an in-order check, then explain why checking only parent against child is wrong
- Write BFS with visited marked on enqueue and state its O(V + E) time and O(V) extra space, then do the same for a two-pointer palindrome that skips non-alphanumeric characters
- For each ActiveRecord query you practise, write the raw SQL beside it and find at least one N+1 pattern in your own code
Panel Interview
Candidates describe an onsite or virtual panel with managers and senior engineers that covers your past work and professional goals. Prepare to explain both the scope of each project and its design trade-offs, and fix the figures you will quote in case more than one person asks about the same project. The sources do not assign the reported design questions to any stage, so review them again before the panel: architecting an application domain from scratch, schema trade-offs for a financial application, and high availability and observability. Go in with a short, specific answer on professional goals that connects to the backend, data and API work the role describes.
What to demonstrate
- Whether you can explain your own design decisions on past projects, including what you rejected and why
- Whether the figures and trade-offs you give for a project stay the same when a different interviewer asks about it
- Whether your professional goals are stated specifically enough to connect to backend, data and API work
How to prepare
- Write a one-page sheet per project with the figures you will quote: traffic, data size, team size, timeline and what broke. Say them aloud until they come out the same every time
- Rehearse one domain-architecture answer from scratch: entities and boundaries, the API surface, the schema, then the failure you are designing for
- Prepare two sentences on professional goals and one example from your past work that supports them
Where candidates lose points
Validating a binary search tree by comparing each node only with its direct children
A tree with root 5, left child 4, and right child 6 whose left child is 3 passes every parent-child check but is not a valid BST, because 3 sits in the right subtree of 5. Pass (low, high) bounds down the recursion, or check that an in-order traversal is strictly increasing. Say how you treat duplicate keys before you code, and state O(n) time and O(h) stack, where h is the tree height and is n for a skewed tree.
Writing ActiveRecord in a live SQL session without being able to say what SQL it runs
One reported prompt asks for SQL and ActiveRecord together, so practise both side by side. For each association call, write the SQL it generates and point out where a loop over records causes an N+1 query. Know which methods eager-load and fix it (includes, preload, eager_load), and that joins is not one of them: it adds an INNER JOIN for filtering but does not load the associated records, so iterating over the association still runs one query per record. Be able to say whether each filter runs in the database or in memory, and what that costs.
Answering the financial-schema trade-off question with normalisation theory and no money-specific decisions
Name the choices that matter for financial data. Store amounts as integer minor units with a currency, not as floats. Decide whether history is append-only or updated in place, and what an auditor can reconstruct under each. Decide which balances are derived and which are materialised. Name the isolation level that stops two concurrent writes from breaking an invariant. The SQL drills in this guide on materialised balances and loan-schedule versioning give you concrete examples of each.
Answering the high-availability and observability question with a list of tools
Start from the failure: which dependency goes down, what the user sees, and how the system recovers. Then state the signals that tell you it happened, such as error rate, latency and the age of the oldest unprocessed item, and where you would alert. The webhook-stall debugging drill in this guide shows the kind of detail that belongs here, such as an alert on the oldest undelivered row instead of on process health.
Telling the panel a project story whose numbers or trade-offs change between interviewers
The panel includes several managers and senior engineers discussing your past work, so the same project may come up more than once. Fix the figures and the key decision for each project in writing beforehand. When you answer the reported question about disagreeing with a lead, say what evidence you brought, what you conceded, and how you committed once the decision was made. An answer that only describes giving in, or only describes escalating, gives the interviewer little to credit.
Questions & practice
Can you validate a binary search tree?
Medium · data structures and algorithms
Approach
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
What are the best practices for specifying API endpoints?
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Choose the data structure from the access pattern, not from familiarity.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
How would you implement a Breadth-First Search (BFS) in a given scenario?
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Explain the two-pointer approach and its time complexity in the context of a palindrome problem.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
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
- Normalise the sign at read time from
direction, not from the amount:signed = +amount_minorfor debit,-amount_minorfor credit (state which convention you picked). The schema constrainsamount_minor > 0precisely so the sign lives in exactly one place. - 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 byhash(account_id) % Pand run P passes for 1/P of the memory. - Ride the zero-sum check on the same pass. Because a transaction's entries are contiguous, keep a tiny
currency -> int64map for the currenttransaction_idonly, test it against zero on the boundary and at EOF, then clear it. That is O(currencies in one transaction), typically one or two. - 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.
- 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.
Worked solution (20 min)
- Write the sign rule down in one sentence before any code, naming which side debit is positive on, and apply it at read.
- Implement with two maps —
balances: (account_id, currency) -> int64andtxn: currency -> int64— plus the currenttransaction_id. - On a change of
transaction_id, assert every currency intxnsums to zero, record the id if not, then clear. - Feed a fixture: one 2-entry transaction that balances; one 4-entry transaction with USD and JPY legs that balances within each currency; one 3-entry transaction off by a single minor unit.
- Re-run with the entries shuffled inside each transaction to prove the result is order-independent within a transaction.
Expected result: The balances map is identical under any within-transaction ordering; exactly the third transaction is reported; the mixed-currency transaction is accepted, because zero-sum is required per currency, not across the transaction.
Verify it yourself
- Across the whole file, the sum of every account balance is exactly 0 per currency whenever no transaction is flagged.
- Two runs over the same file produce byte-identical output.
- Swapping the int64 accumulator for binary64 makes a fixture with amounts above 2^53 minor units disagree — that is a failing test, not a review comment.
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_datewithout a second full scan? - The job is restarted after a crash halfway through the file. What makes the second run produce identical output?
Decide whether balance is derived or materialised, then hold the floor
Hard · denormalisation · isolation levels · lock contention
Authorisation needs an account's available balance inside an 80 ms budget at 3,000 requests per second; that account already has 200 million ledger_entry rows. A product rule says the balance may never fall below the account's negative overdraft limit. Decide whether the balance is summed from entries or held in a materialised account_balance(account_id, balance_minor, floor_minor, currency, version) row, and justify the choice from the read pattern. Then give the write path that holds the floor, naming the isolation level, the anomaly a weaker level permits, and the SQLSTATE you retry.
Approach
- Size the derived read before arguing about it: summing 200 million rows is not an 80 ms operation under any index, since even a covering index on (account_id, entry_id) still reads work proportional to the rows. The authorisation read pattern forces one materialised row fetched by primary key; the statement read pattern, which is low-rate and historical, stays derived from entries. That is the whole justification for the denormalisation, and its price is a write on that row per posting.
- Put the floor where it can be a single-row constraint: CHECK (balance_minor >= floor_minor) on account_balance, updated in the same transaction as the entries. As a predicate over a set of entry rows it cannot be a CHECK at all, which is the reason the materialised row earns its keep twice.
- Write the mutation as one statement: UPDATE account_balance SET balance_minor = balance_minor - $1, version = version + 1 WHERE account_id = $2 AND balance_minor - $1 >= floor_minor. Zero rows updated means refused. This is safe even under READ COMMITTED, because the UPDATE re-evaluates its predicate against the locked, post-update version of the row.
- Name the anomaly in the shape that is not safe: SELECT the balance, compute a new value in application code, then UPDATE to that constant. Every statement under READ COMMITTED takes a fresh snapshot, so two concurrent 60-unit withdrawals against 100 both read 100 and both write 40. The row then claims 40 while 120 has actually left, so the true position is -20, below a floor of 0, and the row it is checked against cannot show it. PostgreSQL REPEATABLE READ is snapshot isolation and aborts the loser with SQLSTATE 40001; SERIALIZABLE additionally closes write skew across two rows. Both require a bounded retry with backoff, and InnoDB REPEATABLE READ does not abort at all, so identical code changes behaviour on a different engine.
- For a two-account transfer, acquire the rows in a deterministic order such as ascending account_id; without it, opposing concurrent transfers deadlock and the database kills one with SQLSTATE 40P01. That is a retry, not a correctness failure, but it is a retry somebody has to write.
- Finish on the ceiling: the hot row admits one committed write per lock hold, so at a 2 ms hold it caps near 500 per second regardless of cores. Sharding into N sub-rows multiplies throughput and immediately makes the floor check cross-row again, which then needs the shard sum under SERIALIZABLE or a per-shard reserved allowance.
Worked solution (35 min)
- Seed one account with balance_minor = 100 and floor_minor = 0, then run two concurrent 60-unit withdrawals under READ COMMITTED using SELECT-then-UPDATE and record the final balance.
- Replace it with the single-statement conditional UPDATE and re-run the same race, asserting on rows affected rather than on an exception.
- Re-run under SERIALIZABLE with the read-modify-write shape, count the 40001 aborts, and add a retry loop with a fixed attempt cap and jittered backoff.
- Measure committed writes per second against that single row, then write the drift query: sum signed entries per account and compare against balance_minor.
Expected result: The READ COMMITTED read-modify-write leaves the balance row at 40 after two successful 60-unit withdrawals, so the entries sum to -20 against a floor of 0 while the row says otherwise. The conditional UPDATE lets one withdrawal commit and reports zero rows affected for the other, ending at 40 with entries summing to 40. Under SERIALIZABLE the read-modify-write shape produces 40001 aborts that the bounded retry resolves to the same refusal.
Verify it yourself
- The refused path returns rowcount 0, not an exception; assert on the rowcount.
- The drift query returns zero difference after the correct run and exactly -60 after the lost-update run, which is the anomaly made visible.
- Under SERIALIZABLE, the retry loop terminates: record attempts per transaction and confirm the cap is reached rarely rather than never being reached at all.
- Reverse the lock order in a two-account transfer test and observe 40P01 rather than a silent wrong answer.
Follow-up
- Shard the balance into eight sub-rows. Write exactly what the floor check now does, and what it costs per authorisation.
- Someone proposes an AFTER INSERT trigger on ledger_entry to maintain the balance. What does that change about ordering, about batch posting, and about failure handling?
- How do you detect that the materialised row has drifted from the entries, how often do you run it, and on which replica?
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
- 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-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?
How would you approach the architecture of an application domain from scratch?
Medium · system design and architecture
Approach
- Name the failure you are designing for, then the recovery path.
- State the consistency you need, and where you are willing to be stale.
- Name the read and write paths separately; they rarely have the same bottleneck.
Follow-up
- How does this behave when that dependency is down for an hour?
- What breaks first when traffic grows ten times?
How do you ensure high availability and observability in a distributed system?
Medium · system design and architecture
Approach
- Choose a partition key and say what query it makes expensive.
- Name the failure you are designing for, then the recovery path.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
Raise the write ceiling on one hot clearing account
Hard · lock contention · isolation levels · sharding · write-heavy
The ledger posts balanced entry sets at 10,000 entries/s into append-only ledger_entry(entry_id, transaction_id, account_id, direction, amount_minor>0, currency, source_type, source_id, business_date, posted_at), roughly three entries per transaction. A materialised row per account carries the floor check available_minor >= floor_minor and is updated in the posting transaction. One pooled clearing account appears in 40 percent of transactions. Compute the throughput ceiling on that one row, then raise it. Name the isolation level each version requires and state precisely what sharding the balance costs the floor check.
Approach
- Compute the ceiling before designing anything. Throughput against a single row is one divided by the lock hold time, where the hold runs from first acquiring the row to commit, including the WAL flush. At a 2 ms hold that is about 500 commits/s on that row, and adding cores does not move it. Demand is 10,000 entries/s at three entries per transaction, so roughly 3,300 transactions/s, of which 40 percent touch this account: about 1,300/s against a 500/s ceiling. You are 2.6x over before any code exists.
- Shorten the hold before sharding, because it is free. Acquire the hot row last in the transaction and hold it for exactly one statement: UPDATE balance SET available_minor = available_minor - $1 WHERE account_id = $2 AND available_minor - $1 >= floor_minor. This is correct at READ COMMITTED, because an UPDATE that blocks on a concurrent writer re-evaluates its WHERE clause against the newly committed row version rather than its original snapshot.
- Name what the broken version needs. SELECT the balance, compute in application code, then UPDATE loses updates at READ COMMITTED, since the second statement takes a fresh snapshot. Making that shape safe requires REPEATABLE READ or SERIALIZABLE plus a bounded retry loop on SQLSTATE 40001, which is the part most implementations omit; SELECT ... FOR UPDATE also works but is simply the same lock held for longer. Note that the identical application code changes behaviour on a different engine: InnoDB's REPEATABLE READ does not abort the loser.
- Order locks deterministically on any two-account movement, always ascending by account_id. Concurrent opposing transfers without that ordering deadlock with SQLSTATE 40P01, and the deadlock rate rises with exactly the concurrency you added to gain throughput.
- Shard the hot balance into N sub-rows keyed (account_id, shard_no), writers choosing a shard by hash of transaction_id. Per-shard hold time is unchanged, so capacity rises roughly N-fold, and at N=16 you clear 1,300/s with headroom. The cost is precise: the floor is now a predicate over the sum of N rows and is no longer expressible as a single-row CHECK, so it must be paid for deliberately. Either give each shard its own floor, which rejects postings while other shards hold headroom, or add a slow path that locks all shards in ascending shard_no under SERIALIZABLE, rebalances and retries on 40001. For a clearing account whose floor is effectively unreachable, dropping the per-shard floor and enforcing it as a monitored invariant is a defensible stated choice; for a customer deposit account it is not.
- Keep the entries immutable throughout: no UPDATE or DELETE grant on ledger_entry plus a trigger, corrections as reversing transactions, and partitioning by business_date so index writes stay in the current partition and retention is a DETACH.
Worked solution (45 min)
- Run two concurrent sessions doing SELECT then UPDATE to a computed value against the same account under READ COMMITTED and observe one update lost.
- Replace it with the single atomic conditional UPDATE and re-run; then run the original read-modify-write under REPEATABLE READ with a bounded retry on 40001.
- Measure committed transactions per second against the single pooled row at 1, 8, 32 and 128 concurrent writers, recording mean lock hold time.
- Shard into 16 sub-rows hashed on transaction_id, re-measure, and write the query that answers the true available balance.
- Run concurrent opposing two-account transfers with and without ascending account_id lock ordering.
Expected result: The read-modify-write loses an update at READ COMMITTED and aborts with 40001 at REPEATABLE READ; the single atomic statement is correct at READ COMMITTED with no retry loop. Single-row throughput flattens near one divided by the lock hold time, about 500 commits/s at a 2 ms hold, against the roughly 1,300 transactions/s this account needs. Sixteen shards clear that, and the floor check becomes a predicate over sixteen rows. Unordered transfers produce 40P01 deadlocks; ascending order produces none.
Verify it yourself
- Past the plateau, adding writers raises latency without raising throughput; that is the ceiling, not a tuning problem.
- After sharding, a floor breach is reproducible when each shard enforces the floor independently; construct it deliberately rather than assuming it cannot happen.
- ledger_entry has no UPDATE or DELETE path throughout, and every correction in the test appears as a new reversing transaction.
Follow-up
- While the balance is sharded, what query answers the account's true available balance, and what does it cost?
- A transaction has legs in two currencies. What does the balanced-to-zero check mean now, and where does the rounding residual go?
- What does the nightly reconciliation compare the sharded balance against?
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
- 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.
- 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.
- 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.
- 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.
- 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
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Recruiter screen and stack mapping
- Map your experience against the listed must-haves (Java, Kotlin or Node.js; SQL and database design; RESTful APIs) and the nice-to-haves (AWS, GraphQL, React), with one project for each
- Write and time a background walkthrough that names those projects, and rehearse it against the question-bank prompt 'Walk Through Your Background'
- List your hard constraints and the questions to ask about the next stage: assessment or phone format, allowed languages, and whether SQL or ActiveRecord is included
Deliverable: A one-page stack map, a rehearsed walkthrough, and a written list of screen questions and constraints.
Day 2 — Trees and graphs from the reported coding set
- Solve 'validate a binary search tree' twice, with bounds and with in-order traversal, and test it on the 5 / 4 / 6-with-left-3 counterexample
- Implement BFS for a grid or graph scenario, marking visited on enqueue, and state O(V + E) time and O(V) extra space for the queue and visited set
- Solve lowest common ancestor from the question bank and explain how the recursion returns found nodes upward
Deliverable: Three working solutions, each with stated complexity and a list of edge cases you tested.
Day 3 — Strings, arrays and explaining your approach
- Write the two-pointer palindrome check, including skipping non-alphanumeric characters, and explain O(n) time and O(1) extra space out loud
- Solve 'first unique character index' from the question bank with a frequency count in linear time
- Practise the question-bank prompt on explaining how you solve coding problems: clarify, brute force, optimise, test, all narrated on one problem
Deliverable: Two solutions and a recorded narration of one problem from clarifying questions to tests.
Day 4 — Live SQL, ActiveRecord and data correctness
- Write five queries against a small schema (joins, grouping, a window function, a filtered aggregate, an upsert) and the ActiveRecord version of each, noting any N+1 risk
- Work the worked exercise 'Decide whether balance is derived or materialised, then hold the floor' and reproduce the lost update in two sessions
- Work the coding exercise 'Derive per-account balances and catch unbalanced transactions' and state its time and space bounds
Deliverable: A sheet of paired SQL and ActiveRecord queries, plus notes on both worked exercises.
Day 5 — API endpoints, OOP and Java depth
- Specify endpoints for one resource: naming, HTTP methods, status codes, pagination, versioning, idempotency keys on writes, and error bodies
- Model the classes for 'design a chess game' and explain where inheritance helps and where composition fits better
- Prepare an in-depth explanation of your main language (for Java: memory model, collections, exceptions), since the bank includes an 'explaining Java deeply' question
Deliverable: An endpoint spec, a class diagram for chess, and a one-page language-depth crib sheet.
Day 6 — Reported design categories
- Architect an application domain from scratch: entities, service boundaries, API surface, schema, then the failure you design for
- Compare two schemas for financial data (in-place updates against append-only history with versioning), using the loan-schedule versioning drill as a reference
- Work the design exercise 'Raise the write ceiling on one hot clearing account', then answer the high-availability and observability question starting from failure modes and alerts
Deliverable: Three design write-ups, each naming a trade-off and the failure it handles.
Day 7 — Panel preparation and a full mock
- Prepare stories for the reported behavioral questions: explaining a technical concept to a non-technical stakeholder, disagreeing with your lead, and pivoting after requirements changed
- Write a one-page figures sheet for each project you will discuss, plus two sentences on your professional goals
- Run a mock panel where two people ask about the same project separately, then compare your two answers for numbers or trade-offs that changed
Deliverable: Written behavioral stories, project figure sheets, a goals statement, and notes from the mock.
Scope is read from what you decided
The panel is described as a discussion of your past work and professional goals with managers and senior engineers, so prepare stories where you made the decision, not stories where you carried out someone else's plan. For each story, state the constraint, the option you rejected, the evidence you used, and the result you can measure. Keep the figures consistent, because more than one interviewer may ask about the same project.
- Describe a time you had to explain a complex technical concept to a non-technical stakeholder.
- Walk me through your background and how it fits this role.
- What are your strengths and weaknesses?
- Describe your project management experience: how you planned the work and handled risk.
How do you handle situations where you disagree with a technical decision made by your lead?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
Tell us about a project where you had to pivot due to changing requirements.
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
- 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?
Resolve a review disagreement over isolation level
Easy · code review · isolation levels · lost update
A colleague's pull request reads a balance, compares it to a floor in application code, then issues an UPDATE with the computed value, all under PostgreSQL's default READ COMMITTED. You comment; they reply that staging has never produced a negative balance. Describe a code review disagreement where you were confident and the author was not convinced. State how you made the failure concrete rather than theoretical, how many rounds it took, at what point you would have escalated or approved anyway, and what you conceded to the author.
Approach
- The probe is whether you can convert a correctness objection into something reproducible instead of a stalemate of opinions. Name the anomaly by its mechanism: under READ COMMITTED each statement takes a fresh snapshot, so two sessions can both read balance 100, both compute 100 minus 80, and both write 20.
- Address the staging evidence directly rather than dismissing it. Staging concurrency on one account row is effectively one, so the absence of the anomaly there is expected under both the broken and the correct implementation. That is the sentence that usually ends the argument.
- Reproduce it in two psql sessions and paste the interleaving into the review. A twelve-line transcript settles in one round what three paragraphs of theory will not settle in four.
- Offer the fix as a choice with its trade-off, not as a verdict: an atomic UPDATE ... SET balance = balance - $1 WHERE account_id = $2 AND balance - $1 >= $3 with a rowcount check keeps it single statement and needs no retry; SELECT ... FOR UPDATE serialises the row and lets you compute in application code; SERIALIZABLE covers the multi-row version of the predicate but requires a bounded retry on SQLSTATE 40001 that someone has to actually write.
- Say where your bar is. Correctness on money is a blocking comment, style is not, and a strong answer states that boundary before the disagreement rather than discovering it during one.
- Name what you conceded. The author was usually right about something (scope, naming, the follow-up being separable), and saying so is what makes the blocking comment land next time.
Follow-up
- The author switches the service to MySQL. Which of the three fixes still behaves the same, and which changes silently?
- The same endpoint later transfers between two accounts. What do you now require in the review?
- How do you keep this from being relitigated in every future pull request?