InterviewLegend

Design a Payment System

Commonly asked
Alex Chen45 min readUpdated 47 real interview reports
In this lesson
  • Approve a loan at checkout inside a latency budget, and book it idempotently
  • Move money through a double-entry ledger in integer cents, posting each entry exactly once
  • Turn a loan into installment charges with a retry-and-remind ladder instead of late fees

Understanding the problem

The prompt

A shopper fills a cart at an online merchant and, at checkout, picks Affirm instead of a credit card. In that moment Affirm has to decide whether to lend them the money, and on what plan: four interest-free payments, or a longer monthly term. If it approves, the merchant is paid in full up front, the shopper walks away with the goods, and Affirm collects the balance back over the coming weeks or months. All of it lands inside the checkout, in under a second, and it moves real money. Design that system.

This design has a calm surface and a dangerous core. The calm surface is the checkout call: a merchant asks for a decision, the shopper taps approve, a confirmation comes back. It looks like any other request and response. The danger is underneath. Every approved checkout sets real money in motion, first out to the merchant and then back from the shopper over time. A system may never lose or duplicate it. Miss a notification and a buyer is annoyed. Miss a ledger entry and someone is short actual dollars. That gap is the whole reason this design is hard, and it is where the interview lives.

Name the hard part before drawing a box. Two things make this system, and neither is the checkout button. The first is the decision. Approve or decline a loan in real time while the shopper waits, against a risk model that must say yes to good borrowers and no to bad ones in a few hundred milliseconds. The second is the money: once approved, funds have to move exactly once, out to the merchant and later back from the shopper, with a record you can audit to the cent. Get the decision and the money movement right and the rest is plumbing. Get either wrong and you lose customers or you lose dollars.

The Affirm angle
This is the most on-brand system Affirm could ask you to design, and on-brand is not the same as asked. Affirm is point-of-sale lending: buy now, pay later, with a real-time credit decision wedged into the checkout of merchants like Amazon, Walmart and Shopify. A payment and lending platform is the machine the whole company runs on, so it is the design worth practicing hardest. Keep the odds straight, though. The Affirm system-design accounts that circulate publicly point to fairly generic prompts, a messaging service and a broadcast pub-sub system among them, and senior candidates sign NDAs, so few specifics get out. Treat this lesson as the on-brand design to rehearse, not a question you are guaranteed to get. It works a latency-bounded decision and exactly-once money movement, the two things Affirm lives on. The practice transfers even if the literal prompt turns out to be a chat app.

Functional requirements

Scope to the loop that carries the weight and name the rest to set it aside. Six things make the core. A merchant opens a checkout for a cart at a fixed price. The shopper picks Affirm, and the system returns a real-time decision with a repayment plan. On approval it books a loan and disburses the funds to the merchant. It records every movement in a ledger it can audit. It schedules the shopper's installments and charges them on time, retrying a failed charge. And a shopper or merchant can read the loan and its balance. Everything else you name and drop.

In scope

  • Return a real-time approve or decline with a repayment plan at checkout, inside a strict latency budget
  • Book a loan idempotently, so a retried or double-submitted checkout never creates two loans for one cart
  • Disburse the funds to the merchant and record the movement in a double-entry ledger, in integer cents
  • Schedule the shopper's installments and charge them on time, retrying a failed charge without piling on fees
  • Post every disbursement and repayment to the ledger exactly once, and reconcile it against the money rails
  • Serve the loan, its plan and its running balance back to the shopper and the merchant

Below the line (name it, then skip)

  • The risk and underwriting models themselves (we call the decision service and honor its verdict, we do not build the model)
  • The card networks and bank rails internally (we hand them a payment instruction, we do not build ACH)
  • Merchant onboarding, merchant-fee pricing and the settlement contracts (a separate business system)
  • Fraud and identity verification (its own system on the decision path, called as a hook)
  • The shopper and merchant UI, and the marketing that put the Affirm button on the checkout page

Non-functional requirements

These are the requirements that pick the design, so put a target on each. The decision is synchronous and the shopper is watching a spinner. The approve-or-decline has to come back in roughly a few hundred milliseconds at the median and under a second at the tail. That is a hard real-time budget, and it is the opposite of a notification system where everything can lag. The money movement is the reverse. Disbursement and collection can be asynchronous, they can take seconds or settle overnight on the rails, but they must be exactly-once and durable. A crash cannot lose a disbursement. A retry cannot double it. Correctness on the money outranks latency on the money every time.

Now the shape of the load. Put Affirm at tens of millions of active shoppers and hundreds of thousands of merchants. Checkouts are spiky: a quiet Tuesday is modest, a Black Friday is a wall. Size the decision path for something like a thousand checkouts a second at peak, each needing a sub-second answer. The number that should worry you is not that rate, though. It is the money. Every approved checkout books a loan and posts a burst of ledger entries. Every live loan then spawns a schedule of future charges, so an active book of tens of millions of loans throws off a steady river of scheduled payments every day. At that volume even a one-in-a-million posting error is real dollars walking out the door, which is why the ledger, not the request rate, is the thing the design is built around.

  • Decision latency: < ~1s at the tail. The shopper waits at checkout, so approve or decline is synchronous and sub-second. This hard budget, not throughput, shapes the whole decision path.
  • Peak checkouts: ~1K/sec. Spiky by nature and brutal on a sale day. Each one needs a real-time answer, so the decision service sheds load with a safe default rather than stalling.
  • Money movement: exactly-once. Disbursement and repayment can be async and settle over hours, but never lost and never doubled. Correctness here outranks speed.
  • Active loan book: tens of millions. Every live loan spawns future charges, so the daily volume of scheduled payments, not the checkout rate, is the load that never sleeps.

The set-up

Core entities

Name the objects a loan is built from before drawing anything. Five carry the design, and the two that decide everything are the ledger and the schedule, not the loan record itself.

1

Checkout

One attempt to pay for one cart at one merchant: a merchant_id, a cart_amount_cents, a currency and a client-supplied idempotency_key. It is the unit the shopper acts on, and the idempotency key is what stops a retried tap from booking two loans.

2

Loan

The credit Affirm extends for an approved checkout. It holds the borrower, the principal in integer cents, the plan (four biweekly payments, or a monthly term) and a status that moves from approved to disbursed to repaying to closed. It is created once, atomically, per checkout.

3

Ledger account and entry

The double-entry record of money. Every account holds a balance in integer cents, and every movement writes two entries that sum to zero, a debit on one account and a credit on another. This append-only log is the single source of truth for where the money is, and it is never edited in place.

4

Repayment schedule

The installments a loan owes, each with a due date, an amount in cents and a state. A scheduler walks it and charges each one when it comes due. This is what turns a one-time approval into weeks or months of collection work.

5

Disbursement and charge

The two directions money flows: a disbursement pays the merchant up front, a charge later collects an installment from the shopper. Each carries its own idempotency key, so the same instruction sent twice moves money once.

The design leans on one line. Money is never a number you overwrite. It is a sequence of balanced entries you append. Ask an account for its balance and you are really asking for the sum of its entries, and every disbursement or collection is two entries booked together, so the books always balance. Framed as a mutable balance column you update in place, money is one lost write away from being wrong and impossible to audit. Framed as an append-only double-entry ledger, every cent has a history and the system can prove itself correct. The whole design is an argument for the second framing.

The stores follow from that. The ledger wants a relational database with real transactions, Postgres or similar. A posting is several rows that must commit together or not at all, and a serializable transaction guarantees that. The loans, plans and schedules sit in the same relational world, since they are read and written together with the ledger. The disbursement and repayment work rides a durable queue so a movement can be retried after a crash. And a scheduler drives the installment charges on their due dates. The Postgres lesson covers why a relational store fits money, and async processing covers the queue and scheduler underneath the movement.

API

The surface is small, and it keeps the synchronous decision apart from the asynchronous money movement. Opening a checkout and asking for a decision are the shopper-facing writes. Reading a loan is a plain read. The disbursement and the scheduled charges are not public calls at all: they happen behind the decision, driven by the ledger and the scheduler. Every write that books a loan or moves money carries an idempotency key, because every one of them can be retried.

The interface
RequestServer pushFire-and-forget
open a checkout for a cart at a merchant
POST /v1/checkout/sessions
201 { session_id, cart_amount_cents, merchant_id }
the merchant starts the session; the amount is fixed here so the decision and the loan agree on it
ask for a real-time decision and book the loan
POST /v1/loans
200 { loan_id, decision, plan }
idempotent on the Idempotency-Key header; a retry returns the same loan and decision, never a second loan
confirm the plan and disburse to the merchant
POST /v1/loans/{id}/confirm
200 { status: disbursed }
idempotent; enqueues the disbursement once, so a double tap does not pay the merchant twice
read a loan, its plan and its balance
GET /v1/loans/{id}
200 { principal_cents, plan, balance_cents, next_due }
a plain read off the relational source of truth, not a scan of the ledger log
money posted to the ledger (internal event)
LedgerPosted { entry_id, loan_id, amount_cents }
consumed by settlement and analytics
emitted after a posting commits, so downstream consumers see only money that truly moved
a scheduled installment comes due (outbound charge)
ChargeDue { loan_id, installment_no, amount_cents }
drives a charge against the shopper on the rails
carries an idempotency key per installment, so a retried charge collects it once

Two things in that surface do the heavy lifting. The idempotency key on every money-moving write keeps the whole thing safe. From the loan booking onward every hop can retry, and a shopper must never end up with two loans or a merchant paid twice. And splitting the confirm from the decision keeps the payout off the shopper's clock: the yes-or-no is synchronous, the actual disbursement is queued and settles behind it.

High-level design

Walk one checkout through the system, simplest correct version first. The diagram colors each box by role. The shopper, the merchant and the external card and bank rails are blue. The services, the checkout service, the credit decision, the ledger service and the repayment scheduler, are violet. The disbursement queue and the repayment charge queue are amber. The double-entry ledger and the loans and plans store are green.

Loading diagram...

1) The shopper picks Affirm. The merchant opens a checkout session for the cart, and the shopper chooses Affirm instead of a card. The checkout service takes it from there. It decides nothing itself. It packages the shopper and the amount and turns to the decision. This is the one leg the shopper actually waits on, so everything here is built for speed.

2) Approve or decline, right now. The credit decision reads the shopper's precomputed risk features and limits from the loans and plans store and returns a verdict and a plan, inside a few hundred milliseconds. It runs under a hard deadline. If the model is slow or a dependency is down, it falls back to a safe default instead of leaving the shopper staring at a spinner. A checkout that hangs gets abandoned.

3) Book the loan and pay the merchant. On an approval the checkout service books the loan exactly once, guarded by the checkout's idempotency key, and posts the disbursement through the ledger service. The ledger writes the balanced double-entry to the double-entry ledger and drops a payout job on the disbursement queue. That job runs off the shopper's clock now: it settles through the card and bank rails and the merchant is paid in full. The shopper already has the goods.

4) Collect the installments over time. The loan is now a schedule. The repayment scheduler walks it and, as each installment comes due, drops a charge on the repayment charge queue. A worker pulls the payment from the shopper through the rails and posts the repayment back to the ledger. A charge that fails is retried on a backoff and the shopper gets a reminder instead of a fine, in line with Affirm's no-late-fees promise. When the last installment clears, the loan closes.

Size the system

Work the arithmetic, because it sets the whole story. Start with the decision path. A thousand checkouts a second at peak, each needing an answer in a few hundred milliseconds, is a demanding but ordinary synchronous service. It holds as long as the risk features are precomputed and read instead of calculated live inside the budget. Cache the features, keep the decision stateless, and this leg scales by adding replicas. It is real-time, but it is not the hard part.

The money is the hard part, and it is a volume story, not a latency one. Every approved checkout posts a handful of ledger entries, and every one of the tens of millions of live loans carries a schedule of future installments. Pay in 4 alone turns one purchase into four dated charges, and monthly plans stretch to a dozen or more. Add it up and the system posts a continuous stream of scheduled payments every day, forever, each one a real dollar amount that has to move exactly once. So the ledger has to stay correct under sustained write load, and the scheduler has to fire millions of charges a day without dropping or doubling one. That, not the checkout rate, is what the design is sized for.

  • Decision path: ~1K/sec, sub-second. A demanding but ordinary synchronous service once risk features are precomputed and cached. Scales by adding stateless replicas behind the budget.
  • Ledger writes: several per checkout. Each approval posts a balanced set of double-entry rows in one transaction. The store is sized for sustained write load, not burst reads.
  • Scheduled charges: millions per day. Every live loan owes future installments, so collection is a steady daily river, not a spike. The scheduler must fire each one exactly once.
  • Money errors: target zero. At this volume a one-in-a-million posting slip is real dollars lost. Exactly-once posting and reconciliation are the non-negotiables.

Technology choices

Each box is a real category of tool, and Affirm is a payments company where the money store is chosen for correctness first. Pick a concrete tool, say why that kind of tool fits, and have the failure question ready.

  • A relational database like Postgres holds the ledger and the loans, because a posting is several rows that must commit atomically and money wants real serializable transactions, not eventual consistency. The failure probe is a concurrent double-post: a unique constraint on the idempotency key plus row-level locking means the second writer is rejected, not merged. The Postgres lesson covers why relational fits money.
  • A durable queue carries the disbursements and the scheduled charges, so the synchronous checkout never waits on a slow rail. The probe is a worker that crashes mid-payout: because the queue is durable and each job is idempotent, the job replays and the rails instruction dedupes, so the merchant is paid once. The async processing lesson covers these guarantees.
  • A scheduler drives the installment charges on their due dates, enqueuing a charge as each one matures. Keep it stateless over durable schedule rows, so it can crash and resume without skipping a due date. This is the same shape as any multi-step workflow that has to survive restarts.
  • The checkout and decision services are stateless and sit on the hot path, scaled by replicas, with the risk features cached so the decision reads rather than computes. Stateless is the point: the loan, the ledger and the schedule all live in the datastores, so any replica serves any checkout and a dying box costs only its in-flight request.
  • The card and bank rails sit outside the system and are treated as best-effort. None can promise an instruction settled just because the call returned, so every payout and charge is idempotent and reconciled against what the rails actually report. This is also why exactly-once across the rails is a fantasy, which the idempotency deep dive works through.

Potential deep dives

The deep dives carry most of the grading. The interviewer picks one property of the system and pushes until it bends, and for a lending system the four below are where it bends. Each maps to a tier an interviewer grades against.

1) The decision: approve or decline before the shopper leaves

This is the leg that makes Affirm a lender instead of a checkout button, and it is the only synchronous step in the design. A shopper is at the merchant's checkout with a cart, and Affirm has milliseconds to decide whether to lend and on what terms. Two things have to be true at once. The answer has to be fast, and booking the loan has to be safe under retries.

Fast first. The decision cannot recompute a heavy risk model from raw history while the shopper waits. It reads features that were computed ahead of time and cached, applies the model, and returns. It also runs under a hard deadline with a fallback, because the worst outcome is not a decline, it is a checkout that hangs. Safe second. The network between the merchant and Affirm will retry, so the same checkout can arrive twice, and without a guard that means two loans for one cart.

BadScore everything live, book on every call

Approach

For each checkout, pull the shopper's full history, run the risk models from scratch, call the bureau synchronously, then insert a new loan row.

Challenges

The tail latency is brutal: one slow dependency and the shopper stares at a spinner past the point they abandon the cart. And with a plain insert on every call, a retried request books a second loan for the same cart, so a network hiccup quietly doubles someone's debt. Correct in a demo, wrong in every way that matters at checkout.

GoodPrecompute features, bound the decision, key the loan

Approach

Compute the expensive risk features offline and cache them per shopper, so the live decision is a fast read plus a model call under a strict deadline. Wrap the loan write in the checkout's idempotency key, so a retry finds the existing loan instead of making a new one.

Challenges

This hits the latency budget and stops the duplicate loan, which is most of the battle. The open questions are what the decision does when its deadline blows, and where exactly the idempotency key is enforced so two in-flight retries cannot both win the race. Good, but the failure and race behavior still needs pinning down.

GreatA deadline with a safe default, and an atomic idempotent booking

Approach

Serve risk features from a fast store keyed by shopper, and give the decision a hard timeout with an explicit safe default, hold or decline, when a dependency misses the budget. Enforce the idempotency key as a unique constraint on the loan write itself, so of two racing retries the database admits exactly one and the other reads back the same loan. Return the plan from the decision; disbursing happens only on the separate confirm.

Challenges

What remains is choosing the safe default on purpose: a hold is safer than a wrong approve, but it costs conversion. Scope the key to the cart instead of the session, so a genuine second purchase is not swallowed as a duplicate. This keeps the common case a sub-second read and makes the loan booking provably single even under retries.

The handling contention lesson covers the idempotency key and the atomic write that make the booking single, the same tool the ledger uses to post money exactly once.

The Affirm angle
The sub-second yes or no is the single most Affirm-specific thing in this design, so do not wave at it with a generic payment gateway. Point of sale is the whole model. The decision competes with the shopper's patience and the merchant's conversion rate, so a hundred milliseconds of latency and a point of approval rate are business numbers. This is also where Affirm's coding round and its system design meet. The trailing-hour aggregations Affirm favors in its coding problems, summing a shopper's loan volume over the last hour, are exactly the kind of precomputed feature this decision reads instead of recomputing live. Name the deadline, name the safe default, and treat the approval rate as a dial you are tuning, and you are speaking Affirm's language.

2) The ledger: move money without losing a cent

Once a loan is approved, money starts moving, and money is the one part of the system that cannot be eventually correct. It has to be correct now and forever, and it has to be auditable, because a regulator or a reconciliation job will ask where every cent went. The question this deep dive answers is how to record money so that it is impossible to lose, impossible to double, always provable.

BadOne balance column, updated in place

Approach

Give each account a balance field and, on every movement, read it, add or subtract, and write it back. Disbursing debits Affirm and credits the merchant by updating two numbers.

Challenges

Two concurrent updates race and one overwrites the other, so a balance silently goes wrong with no way to notice. There is no history, so nothing can be audited or reconciled, and a float column rounds fractions of a cent into real drift across millions of rows. This is how you lose money you cannot even find.

GoodAppend-only double-entry in integer cents

Approach

Never overwrite a balance. Append two entries per movement, a debit and a matching credit that sum to zero, each an integer number of cents, and derive an account's balance from the sum of its entries. Book both entries in one transaction.

Challenges

Now every cent has a history, the books always balance, and rounding is gone because there are no fractions to round. The remaining risk is the same movement posted twice from a retry, and deriving a hot account's balance by summing a long history gets slow, so the running balance needs maintaining as movements land.

GreatExactly-once posting with a maintained balance

Approach

Keep the append-only double-entry log, and post each movement inside a serializable transaction guarded by an idempotency key held as a unique constraint, so a replayed instruction is rejected rather than posted twice. Update the running balance in the same transaction as the entries, so the balance and the log can never disagree. Reconcile the ledger against what the rails report on a schedule.

Challenges

The cost is contention on hot accounts, since serializable postings to the same account serialize, which is answered by keeping transactions tiny and, where needed, sharding movement across sub-accounts. Reconciliation has to handle the rail that accepted a payment but reports it a day later. Within the system this is airtight to the cent; the honest gap is the outside rail, which the next dive takes on.

Money is integers and money is entries, never floats and never a column you overwrite. Store cents as integers, because a float rounds a third of a cent somewhere and, multiplied across millions of postings, that rounding becomes a real number an auditor will find. And record every movement as two balanced entries rather than editing a balance, because a mutable balance has no history and no defense against a lost update. The same discipline runs through Affirm's coding round, where amounts are integer cents and a running total is something you maintain, not recompute. If you can always say where a cent came from and where it went, the design is sound.

3) Idempotency and the rails: at-least-once without moving money twice

The ledger is airtight inside the system. The trouble starts at the boundary, where money crosses into networks Affirm does not own. A payout call to a bank can time out after the bank already moved the funds. A charge can succeed while the acknowledgment is lost on the way back. From the outside, done and never-happened look identical, so the sender has to retry, and retrying is how you pay a merchant twice. Two failures pull in opposite directions: losing a movement, and duplicating it.

Fire the instruction, trust the return

  • Sends the payout or charge once and believes whatever the rail returns
  • A timeout after the money already moved reads as a failure, so a retry moves it again
  • No stable key, so the system cannot tell a retry from a genuine second payment
  • Merchants get paid twice and shoppers get charged twice, and the ledger cannot prove which is real

Idempotency key, then reconcile

  • Every payout and charge carries a stable key the rail echoes back, so a retry is recognized
  • Retry freely on a timeout: the key makes a duplicate instruction a no-op at the rail or the ledger
  • A daily reconciliation matches ledger postings against the rail's own settlement report
  • A movement the rail confirms late is matched by key, not double-counted, so the books stay exact

The mechanism is the same idempotency key the loan booking and the ledger already use, carried one hop further, out to the rail. Attach a key derived from the loan and the specific movement, retry as hard as you need, and let the key collapse any duplicate to a single effect. Then reconcile. The rail is the one actor that can accept money and confirm it hours later. Only a comparison against its settlement report catches a payout that silently failed or landed twice.

Exactly-once across a network you do not own is a fantasy, so stop reaching for it. A bank can move funds and lose the acknowledgment, and from the outside that is indistinguishable from never moving them. You have to retry, and retrying can duplicate. The honest, standard answer is at-least-once plus an idempotent sink: retry all you want, and make the second instruction a no-op with a key the rail honors. Back it with reconciliation against the rail's report, and you get money that moves once and books you can prove.

4) Repayment: the schedule, the retries, the reminders

Disbursement is the easy direction, one payout and done. Collection is the long tail. A single Pay in 4 loan owes four charges over six weeks, a monthly plan owes a dozen or more over a year, and cards expire, balances run short and charges fail. The system has to drive every installment to its due date, handle the ones that fail without punishing the shopper, and keep them informed, all while Affirm's brand promises no late fees.

  • Drive it from a scheduler over durable rows. Each installment is a row with a due date and a state, and a scheduler enqueues a charge as each one matures. The schedule is the source of truth, so the scheduler can crash and resume without skipping or repeating a due date.
  • Make every charge idempotent. A charge carries a key per installment, so a retried or redelivered charge collects that installment exactly once. This is the same guard the disbursement and the ledger use, pointed at collection.
  • Retry on a ladder, do not fine. A failed charge retries on a backoff over days, a dunning ladder, rather than giving up or stacking penalties, because Affirm charges no late fees. The shopper is nudged to update a card or add funds, not punished.
  • Notify around each attempt. Remind the shopper before an installment is due, confirm when it clears, and flag a failure with a way to fix it. The reminders do real work. They are how a no-late-fees lender keeps its loss rate down.
  • Reconcile the collection too. A charge the rail settles late is matched back to its installment by key, so the loan's balance reflects money that truly arrived, not money that was merely attempted.
The Affirm angle
Affirm's whole brand is no late fees and no hidden fees, which turns repayment from an afterthought into a core system. A lender that profits from penalties can be lazy about collection and let fees pile up. Affirm cannot, so a missed installment is an engineering problem, a retry-and-remind loop, not a revenue event. So the dunning ladder, the reminders and the autopay path carry real weight. They are how the business keeps losses low without the fees its competitors lean on. Saying that out loud shows you understand what makes Affirm's version of this system different from a plain payment processor.

What is expected at each level

That is a wide surface for one prompt, and how much of it you have to land scales with the level. Affirm gates this round, lighter for a mid-level engineer and a real bar from senior up. One more note on the loop. Public accounts suggest senior candidates can see two system-design rounds, while staff loops fold in a project walkthrough. At senior and above, expect to defend a design like this in depth, possibly more than once.

Software Engineer 2

Breadth carries it. Get to a correct pipeline: a checkout asks for a real-time decision, an approval books a loan and disburses to the merchant, and a schedule collects the installments back. Say why the decision is synchronous and the money movement is not. Know that money lives in a double-entry ledger in integer cents, and that booking a loan and moving money need an idempotency key so a retry does not double either. The reconciliation and the failure paths are the interviewer's to raise. The bar is a clean end-to-end path and the instinct that the ledger, not the checkout button, is the hard part.

Senior Software Engineer

Cover the breadth, then own the decision and the ledger. You are expected to reach the double-entry, integer-cent ledger without prompting. Put a hard deadline and a safe default on the decision, and guard the loan booking and every money movement with an idempotency key. Explain why exactly-once across the rails is off the table and reach for reconciliation instead. Treat a failed repayment as a retry-and-remind problem, not a fee. The bar is a design that stays correct and auditable under retries and a slow rail, with the relational money store justified as a deliberate choice. With two design rounds reported at this level, be ready to hold up under a second, different prompt too.

Staff Software Engineer

Depth everywhere the system bends, and a widening frame. Lead on the money: exactly-once posting under contention, the running balance kept in the same transaction as the entries, and reconciliation against a rail that settles late. Reason about the consistency between the synchronous decision and the asynchronous disbursement, and how the schedule fires millions of charges a day without dropping one. See the general money-movement platform under the checkout prompt, the same ledger and idempotency spine that would serve the Affirm Card or a refund flow. If the loop includes a project walkthrough, be ready to narrate a real system you owned at this depth. The bar is that correctness, latency and the no-fees repayment all stay central at once, and you drive while the interviewer redirects rather than rescues.

Key Takeaways

  • ✓The design has one synchronous leg and one durable core. Approve or decline in real time on the shopper's clock, under a hard latency budget with a safe default, and book the loan idempotently so a retried checkout never creates a second loan. Everything after the decision runs off the shopper's clock.
  • ✓Money is a double-entry, append-only ledger in integer cents, not a balance you overwrite. Every movement is two entries that sum to zero, posted exactly once behind an idempotency key inside a serializable transaction. The running balance updates in that same transaction, so the log and the balance can never disagree.
  • ✓You do not own the rails, so at-least-once plus an idempotent sink plus reconciliation is the honest guarantee. Carry the same idempotency key out to the bank and retry freely; the key collapses duplicates to one effect. Reconcile against the rail's settlement report to catch a movement that failed or landed late.
  • ✓Repayment is a first-class scheduler, not an afterthought. Installments are durable rows a scheduler drives to their due dates with idempotent charges, a retry-and-remind ladder replaces late fees, and reconciliation confirms the money truly arrived. Affirm's no-late-fees promise is what makes the repayment experience the product.
Had your Affirm interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Affirm interview guide

Know what Affirm asks before you walk in.

8 coding · 2 system design · 13 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.