Revolut System Design interview
This blog breaks down how to ace Revolut’s System Design interview by designing around a correctness-first ledger, safe retries, FX and card workflows, and reconciliation when external payment truth arrives late.
Revolut builds fintech systems where “it worked in prod” is meaningless unless the money is correct, the audit trail is defensible, and the business can reconcile against external rails weeks later. The system design interview is designed to see whether you can reason about financial invariants under distributed failure, not just scale an API.
In a Revolut interview, you’re not only designing a service. You’re designing a financial machine: a ledger as source of truth, derived projections for speed, risk gates that can stop or delay money movement, and operational controls that keep humans and regulators confident when the network lies to you.
System Design Interviews decide your level and compensation at top tech companies. To succeed, you must design scalable systems, justify trade-offs, and explain decisions under time pressure. Most candidates struggle because they lack a repeatable method. Built by FAANG engineers, this is the definitive System Design Interview course. You will master distributed systems building blocks: databases, caches, load balancers, messaging, microservices, sharding, replication, and consistency, and learn the patterns behind web-scale architectures. Using the RESHADED framework, you will translate open-ended system design problems into precise requirements, explicit constraints, and success metrics, then design modular, reliable solutions. Full Mock Interview practice builds fluency and timing. By the end, you will discuss architectures with Staff-level clarity, tackle unseen questions with confidence, and stand out in System Design Interviews at leading companies.
Revolut-style system design is about proving you can keep correctness under retries, partitions, and late-arriving external truth—without losing throughput or auditability.
What Revolut optimizes for | What interviewers probe | What strong candidates do |
Financial correctness at scale | Ledger modeling, invariants, reversals | Put the ledger at the center and make everything else derived |
Low-latency card + payments | Auth/capture timing, concurrency | Separate “decision path” from “posting path” while keeping idempotency |
Compliance + auditability | Immutable trails, retention, access controls | Model append-only events, correction entries, and long-lived traceability |
Global + multi-region | Data residency, failure isolation | Explicit region boundaries and “can this fail independently?” reasoning |
Summary (after the explanation):
Treat the ledger as the source of truth, not “a database table.”
Show your invariants before you show your microservices.
Make failure behavior a first-class design output, not a footnote.
Why Revolut’s system design questions feel different#
Most companies reward you for designing a service that’s fast and scalable. Revolut rewards you for designing a system that stays financially correct when the real world misbehaves: partners time out, card networks deliver duplicates, FX rates move mid-flow, and compliance requires you to explain every cent.
A typical Revolut prompt might sound like “design multi-currency ledger + card payments,” but the real test is whether you naturally reach for the constraints Revolut lives with daily: idempotency boundaries across rails, audit trails you can defend years later, and reconciliation when external settlement is delayed or disputed.
Interviewers aren’t looking for the fanciest architecture. They’re listening for whether you treat money movement like a state machine backed by an append-only ledger, with explicit correctness rules.
“Eventual consistency is fine” | Some flows demand strong posting semantics | “Strong consistency for ledger writes, derived reads can be eventual” |
“Retries are harmless” | Retries can double-charge and create disputes | “Idempotency keys at every boundary, plus dedupe in the ledger” |
“Partner response is truth” | Partner truth arrives late and can change | “Operational state now, financial truth later via reconciliation” |
Summary (after the explanation):
Revolut questions are “finance-first distributed systems.”
Treat partners as unreliable and late.
Make correctness legible: invariants, state transitions, and audits.
Step one: Clarify requirements like a financial engineer#
Start by making the interview feel like a production design review. You want to surface requirements that change the architecture: what must be strongly consistent, what can be derived, what is reversible, what requires a human review, and what must be retained for years.
Instead of asking generic questions, tie your questions to Revolut workflows: “Is this a card authorization (temporary hold) or a capture (posting)?” “Is FX rate locked at quote time or at posting time?” “Do we need data residency boundaries per regulatory zone?” “What’s the allowed reconciliation lag with external settlement files?”
A strong opening is not a list of questions. It’s a short story: “If we time out after we’ve posted to the ledger but before we respond, how do we prevent a second debit?”
Correctness | “What are the non-negotiable invariants?” | Determines ledger rules, atomicity, and reversal strategy |
Latency | “Auth path budget: 100–300 ms globally?” | Forces caching, regional routing, and precomputed risk features |
Risk | “Do we block, step-up, or allow then review?” | Shapes risk gates and asynchronous workflows |
Compliance | “Retention: seven years? WORM storage?” | Drives immutable logs, access control, and storage tiering |
Geography | “Data residency by legal entity/region?” | Implies multi-region isolation and controlled replication |
Summary (after the explanation):
Ask questions that change data consistency, posting, and reversibility.
Tie requirements to auth/capture/FX/risk/compliance.
Make reconciliation lag and partner unreliability explicit early.
Step two: Identify the core Revolut workflows#
After requirements, anchor the system around a few high-value workflows that Revolut systems repeatedly implement: multi-currency balance management, card authorization/capture, FX quoting and conversion, fraud/risk decisions, and compliance-grade auditability. You’re proving you understand what moves money and what can stop money from moving.
A good way to structure this is to pick one “happy path” and then immediately show how it fails safely. For example: card authorization is a low-latency decision that may place a hold; capture posts a final ledger entry later; settlement arrives later still and can disagree; disputes introduce reversals and chargebacks. Each step touches different systems and has different correctness requirements.
Revolut interview language you should use naturally: “ledger is source of truth,” “derived projections,” “risk gate,” “idempotency boundary,” “audit trail,” “multi-region failure isolation,” “data residency.”
Card authorization | Approve/decline in milliseconds | Create/adjust a hold | Timeouts, duplicate auths, partial approvals |
Capture/clearing | Finalize amount and merchant | Post final debit, release hold | Late captures, incremental captures, mismatches |
FX quote + exchange | Lock rate and fees | Post two legs + fees | Volatility, quote expiry, slippage handling |
Bank transfer | Route + compliance checks | Pending → posted on confirmation | Delayed confirmations, recalls, rejects |
Fraud/risk | Block/step-up/review | Possibly freeze funds or hold | Model outages, false positives, audit demands |
Summary (after the explanation):
Show one end-to-end flow, then show where reality breaks it.
Separate operational steps from financial posting.
Always connect workflow steps back to ledger entries and reversals.
Step three: Define a robust and auditable data model#
Your data model is where Revolut interviews are won or lost. The interviewer wants to see that you don’t model balances as “a number in an accounts table.” Revolut-grade systems treat balances as derived from append-only entries, with clear invariants and traceability.
Start with the ledger primitives: accounts (often multi-currency sub-accounts), journal entries, postings (debits/credits), and references that tie every posting back to an originating intent (card auth, capture, transfer, fee, refund). From there, you build derived projections: available balance, booked balance, per-merchant spend, risk features, statements, and compliance reports.
If you can’t reconstruct a user’s financial history from immutable events, you don’t have a financial system—you have an API with a liability problem.
Account (per currency) | A ledger bucket with an owner and currency | Prevents cross-currency confusion and supports residency rules |
Journal entry | A single financial “fact” | Append-only evidence for audits and disputes |
Posting | Debit/credit lines within a journal entry | Enables double-entry balancing and invariants |
Transaction reference | External + internal identifiers | Links retries, partner messages, and reconciliation |
FX rate snapshot | Rate + spread + timestamp + source | Explains why two legs posted the way they did |
Summary (after the explanation):
Balances should be derived; the ledger is the source of truth.
Every posting must be traceable to an intent and external reference.
Model FX rates as snapshots, not “current rate at read time.”
Double-entry ledger modeling and invariants Revolut cares about#
Double-entry isn’t a buzzword in these interviews—it’s your correctness framework. Revolut systems often support multi-currency, holds, fees, refunds, chargebacks, and cross-entity movements. Double-entry gives you a way to prove that money doesn’t magically appear or disappear, even when events arrive out of order.
In practice, you’ll model each financial “fact” as a journal entry with postings. A card authorization hold might move funds from “available” to “held” (internal accounts), while a capture moves from “held” to “settled expense,” possibly with a separate fee posting. A refund reverses a prior capture, and a chargeback might post to a dispute suspense account pending investigation. The key is that every movement has a counter-movement.
What interviewers listen for on correctness: you don’t just say “double-entry,” you state invariants and show how you enforce them in code and data constraints.
Every journal entry balances (sum debits = sum credits) per currency | Transactional write + validation in posting service | “Money creation/destruction,” unreconcilable books |
Immutability of posted entries | Append-only storage, correction entries | Audit trail becomes legally weak; disputes become unprovable |
Idempotent posting per business intent | Unique constraint on (ledger_account, business_ref, sequence) | Duplicate debits/credits under retries |
Holds cannot exceed available funds | Atomic check-and-post with isolation | Negative available balances; overspend during concurrency |
FX legs use the same rate snapshot | Rate locked in intent and stored with entry | “Phantom FX,” user-visible mismatch, P&L drift |
Clear separation of operational vs financial state | Operational state machine + ledger as final fact | Systems disagree, reconciliation impossible |
Summary (after the explanation):
Double-entry is your proof system; invariants are your contract.
Enforce invariants at write time, not by offline repair.
Prefer correction entries over overwrites for defensible audits.
Step four: Propose a secure, scalable Revolut-style architecture#
A strong architecture answer starts with the ledger write path and builds outward. You want to show a “thin” decision path for real-time latency (especially cards) and a “thick” posting and audit path for correctness. Revolut systems usually split transactional writes from read-optimized projections and analytics.
A common pattern is: API gateway → domain service (cards/transfers/FX) → risk gate → intent creation → ledger posting → event emission → derived projections. Partners and rails integrate through dedicated adapters, and every external message is treated as untrusted input that must pass idempotency and validation.
Say this out loud: “The ledger is the source of truth. Everything else is a projection or an integration boundary.”
Ingress/API | AuthN, device signals, rate limits | “Idempotency at API boundary; don’t trust clients” |
Domain service | Build intents + state machine | “Operational state is not financial truth” |
Risk gate | Fraud scoring + policy checks | “Fail closed for high-risk, degrade gracefully for low-risk” |
Ledger service | Validate invariants + post entries | “Strong consistency, append-only, unique constraints” |
Event bus | Fanout for projections and audit | “Exactly-once is hard; design idempotent consumers” |
Projections | Available balance, statements, alerts | “Derived, replayable, region-scoped” |
Audit/compliance | Immutable logs + access controls | “WORM storage, segregation of duties, long retention” |
Rail adapters | Card networks, bank rails, webhooks | “Retries, dedupe, and late confirmations are normal” |
Summary (after the explanation):
Build from the ledger outward; keep projections derived.
Put risk gates before posting where possible, but plan for async review.
Treat rails as unreliable and design for late truth.
Step five: Address real-world fintech complexity with failure-mode reasoning#
This is where you move from “design” to “engineering.” Revolut interviewers often ask “what happens when…” because the system’s behavior under failure is the real test. You should proactively walk through a few failure stories and narrate the system’s decisions, invariants, and recovery paths.
Your goal is not to list edge cases. It’s to show that you can keep the ledger correct while still operating: retries should not double-post, FX should not create mismatched legs, and partner delays should not corrupt state. You also want to show operational controls: observability, alerts, manual tooling, and reconciliation workflows.
The best candidates don’t promise “no failures.” They promise “no silent financial corruption,” and they show the mechanisms.
Duplicate submissions | Client retries, network timeouts | Idempotency keys + ledger dedupe constraints |
Partial failures | Posted but response lost | Return by idempotency lookup; never “try again blindly” |
FX volatility | Rate changes between quote and post | Rate snapshot + expiry + slippage policy |
Partner delays | Clearing/settlement late | Operational state pending; financial truth via reconciliation |
Risk service outage | Model store down | Degrade with rules, or fail closed by segment |
Multi-region incident | Partition or region outage | Failure isolation + region-scoped ledgers + controlled recovery |
Summary (after the explanation):
Explain failure behavior as a walkthrough, not a list.
Make “no silent corruption” your north star.
Always tie recovery back to idempotency, invariants, and reconciliation.
Failure-story walkthrough 1: Duplicate submissions plus partial failures#
Imagine a user taps “Exchange 500 EUR to USD” and the app sends POST /fx/execute. The request reaches the API, passes authentication, and the FX domain service creates an intent with an idempotency key (from the client) and a server-generated business reference. It then posts the double-entry journal entry: debit EUR account, credit USD account, plus a fee posting.
Now the network blips right after the ledger commit but before the API returns. The client retries. Without a rigorous design, you risk a second posting and a duplicated exchange. In a Revolut-grade design, the retry hits the API and is mapped to the same idempotency record. The system does not “run the exchange again.” It fetches the already-created intent outcome (and ledger reference) and returns the same result deterministically.
What you want to say in the interview: “If the ledger commit succeeded, retries must return the same outcome without re-posting. That’s an idempotency boundary backed by unique constraints.”
1 | Client sends execute with idempotency key | Client retries become detectable |
2 | Server stores intent keyed by (user, key) | “Intent is unique” |
3 | Ledger posts with unique business reference | Dedupe enforced at source of truth |
4 | Response lost | Safe because commit is durable |
5 | Client retries | API returns by looking up intent outcome |
Summary (after the explanation):
Persist intent before posting and keep it idempotent.
Enforce dedupe at the ledger, not only at the API.
Retries should read results, not re-run side effects.
Failure-story walkthrough 2: FX rate volatility mid-transaction#
Consider a card purchase in USD for a customer whose base balance is in EUR. The authorization arrives at time T with amount USD 100. You may need to reserve EUR equivalent funds. But FX is volatile; the rate at T might differ from the rate at capture time (hours or days later), and capture can be for a different amount.
A strong design makes the policy explicit. One approach: lock an FX rate snapshot at authorization for the hold, then at capture post the final amount using either the original snapshot (if within policy) or a new snapshot with a transparent adjustment entry. Another approach: hold in the transaction currency if possible and do conversion at capture/settlement. Either way, the key is: you never “look up the current rate” at random points. You bind postings to a stored snapshot so the ledger can explain itself.
Interview language that lands well: “FX conversion is a two-leg posting using a stored rate snapshot. If rate changes, we post adjustments explicitly rather than hiding it in a recalculated balance.”
Lock rate at auth | Low volatility or short window | Hold uses snapshot | Predictable, but needs expiry logic |
Convert at capture | Auth is just a hold | Final posting uses snapshot at capture | More accurate but less predictable |
Adjustment entry | Capture differs from hold | Post delta as separate entry | Auditable explanation of slippage |
Summary (after the explanation):
Bind postings to an FX rate snapshot.
If amounts change, use explicit adjustment entries.
Don’t let “current FX rate at read time” leak into correctness.
Failure-story walkthrough 3: Partner delays and late confirmations#
Card networks and bank rails don’t always confirm quickly. You might authorize successfully, then wait a long time for capture/clearing. For transfers, you might submit to a partner and get an ACK, but final confirmation arrives later via batch files or delayed webhooks. If you treat “ACK received” as “money moved,” you’ll create a reconciliation nightmare.
A Revolut-ready design splits operational state from financial state. Operational state tracks what you attempted and what partners told you so far. Financial state is what the ledger has posted as final facts. Your system can show “pending” in the UI while the ledger holds funds, and later reconcile to settlement truth. If the partner sends a late rejection, you don’t edit history—you post a reversal or correction entry.
A reliable mental model: “External truth arrives late. The ledger records our best-known facts now, plus the mechanisms to correct later.”
Submit transfer | SUBMITTED | Optional pending/suspense posting | “Pending” |
Partner ACK | ACCEPTED | No final posting yet | Still pending |
Settlement file arrives | SETTLED/REJECTED | Final post or reversal | Moves to “Completed” or “Failed” |
Summary (after the explanation):
Model operational state separately from final ledger facts.
Expect late truth and design explicit corrections.
Keep UI state honest: “pending” is a feature, not a bug.
Idempotency, retries, and deduplication across external payment rails#
In fintech, retries are not a corner case. They’re the default behavior of clients, networks, and partners. Your job is to define idempotency boundaries and enforce them consistently from the API down to the ledger and out to partner webhooks.
A good Revolut answer states where idempotency is guaranteed, where it cannot be guaranteed, and how you contain damage. For example, you can guarantee idempotent ledger posting for a given business reference, but you cannot guarantee a card network won’t send a duplicate notification. So you make consumers idempotent and ensure that external callbacks map to the same internal reference.
If you say “we’ll make it exactly-once,” expect pushback. If you say “we design idempotency at each boundary and make consumers dedupe-safe,” you’ll sound senior.
A retry-storm scenario (step-by-step)#
A “retry storm” is when one slow dependency triggers exponential retries across layers.
A card network sends authorization requests. Your edge is healthy, but the risk service is slow due to a feature store issue. Auth processing time creeps up.
The card network times out (their timeout is often shorter than you’d like). They retry the same auth with the same network identifiers.
Meanwhile, your API layer has also retried the internal call to the risk gate. Now you have duplicates within your system and duplicates from outside.
Some requests reach ledger posting, some time out before response, and some get declined due to risk gate fallback. Without dedupe, you might place multiple holds.
The storm continues until you apply backpressure, tighten timeouts, and force deterministic behavior: “same request → same outcome.”
What to emphasize: “We cap retries, apply exponential backoff, and shift to ‘lookup-by-idempotency’ rather than ‘re-execute’ once an intent exists.”
Client → API |
+ user scope | API idempotency store | Dedup client retries |
API → domain intent | Server-generated business_ref | Intent store | Links all downstream effects |
Domain → ledger |
+ sequence | Ledger unique constraint | Prevent double-posting |
Domain → partner |
| Partner adapter store | Safe resubmission |
Partner → webhooks |
| Webhook inbox/dedupe table | Idempotent consumption |
Event bus consumers |
| Consumer offset store | Prevent double-apply in projections |
Summary (after the explanation):
Define idempotency keys per boundary; don’t reuse one key everywhere blindly.
Persist intent early and make retries resolve by lookup.
Make every consumer dedupe-safe because duplicates are inevitable.
Reconciliation, settlements, and disputes#
Reconciliation is where “distributed systems” meets “accounting reality.” External rails often finalize financial truth later: card settlement files, bank transfer confirmations, partner adjustments, and chargebacks. The interviewer wants to see that you can build a system that behaves correctly when the final truth arrives late and sometimes contradicts earlier assumptions.
You reconcile by treating the ledger as your source of truth and external rails as external truth sources that must be mapped to your ledger references. The mapping itself is a first-class problem: you need stable identifiers, inboxing of external messages, and deterministic matching rules. When mismatches appear, you create adjustment entries and route exceptions to ops tooling.
“Reconciliation” is not a cron job. It’s a continuous correctness loop with deterministic matching and explicit exception handling."
Authorization | Real-time network message | Fast path | Place hold, store network refs |
Capture/Clearing | Merchant submits later | Merchant delay | Post capture, release/adjust hold |
Settlement | Batch files/clearing reports | Network batching | Confirm final amounts and fees |
Refund | Merchant-initiated later | Business process | Post refund and link to original |
Chargeback | Dispute workflow | Card scheme timelines | Move to dispute accounts, investigate, post outcome |
Operational state vs financial state (the table interviewers love)#
A strong candidate explicitly separates “what we think is happening” from “what is financially final.”
Auth | APPROVED/DECLINED | Hold posted or not posted |
Capture | CAPTURE_REQUESTED/CONFIRMED | Final debit posted + hold released |
Settlement | SETTLEMENT_PENDING/RECEIVED | Settlement-confirmed entry or adjustment |
Refund | REFUND_PENDING/COMPLETED | Credit posted linked to original |
Chargeback | DISPUTE_OPEN/WON/LOST | Dispute postings + final resolution entries |
The phrase to use: “Operational state drives user experience; the ledger drives financial truth and auditability.”
Summary (after the explanation):
External truth arrives late; design deterministic matching and adjustments.
Keep operational state and financial state separate.
Build exception paths: disputes, mismatches, and manual review tooling.
Example prompt: Design a real-time multi-currency ledger for Revolut accounts#
Start by narrating the happy path: a user holds EUR and USD balances, wants to exchange and then spend on a card. Your architecture should show the ledger at the center, projections for reads, and risk gates for decisions.
Then “show” the core flows. An FX execute creates an intent, stores an FX rate snapshot, and posts a journal entry with two legs and fees. A card authorization checks available funds (derived projection), runs risk, and posts a hold entry. Capture posts final entries later and triggers reconciliation when settlement arrives.
If you only talk about services, you’ll sound generic. If you talk about posting rules, idempotency, and reconciliation, you’ll sound like a Revolut engineer.
Intent service | Creates immutable intent with references | Retries resolve by lookup |
Ledger posting service | Validates and posts journal entries | Double-entry invariants + idempotency |
Balance projection | Materialized available/held/booked | Rebuildable from events |
FX rate service | Provides quote + snapshot | Snapshot stored with postings |
Risk gate | Fraud + policy checks | Deterministic outcomes under retry |
Reconciliation service | Matches external truth to internal refs | Adjustments are explicit entries |
Summary (after the explanation):
Model intents, then post to the ledger, then project.
Keep FX snapshots attached to postings.
Reconciliation and disputes are part of the core, not “later.”
What a strong candidate answer sounds like#
You want your answer to sound like you’ve operated systems like this: crisp invariants, explicit boundaries, and specific failure behavior. Avoid vague claims (“we’ll ensure consistency”) and replace them with enforceable mechanisms (“unique constraint on business_ref prevents double-posting”).
“I’ll treat the ledger as the source of truth with append-only journal entries and double-entry validation. All reads come from derived projections that can be rebuilt. I’ll define idempotency boundaries at the API, intent, ledger, and partner layers, and I’ll model operational state separately from financial state because settlement arrives late. Failures should never cause silent financial corruption; they should produce deterministic outcomes or explicit correction entries with audit trails.”
Did I state invariants before architecture? | Correctness-first thinking |
Did I define idempotency boundaries and keys? | Real-world resilience |
Did I separate ledger writes from projections? | Scalability without sacrificing truth |
Did I cover auth/capture/settlement timing? | Payments domain maturity |
Did I explain reconciliation and disputes? | Operational reality awareness |
Summary (after the explanation):
Sound deterministic: invariants, constraints, and state machines.
Make failures explicit and survivable.
Tie everything back to auditability and external reconciliation.
Final thoughts#
If you want to do well in the Revolut System Design interview, aim to be the engineer who makes the system provably correct under messy conditions: retries, duplicates, FX volatility, and late settlement truth. Put the ledger at the center, speak in invariants, and show how your design behaves when the network and partners misbehave.
The best answers feel like a production design doc narrated aloud: clear posting rules, explicit idempotency, derived projections for speed, risk gates for safety, and reconciliation pipelines for reality. If you can tell that story with confidence—and with concrete failure walk-throughs—you’ll stand out.
Happy learning!