This question evaluates understanding of transactional system design, data modeling for financial domains, API design, idempotency, concurrency control, immutable audit trails, and security considerations in an online banking context.
Design and implement a basic online banking system that supports creating accounts, deposits, withdrawals, and transfers between users. Define the data model and APIs, ensure atomicity and idempotency of money movements, prevent double spending under concurrency, maintain an immutable ledger/audit trail, and address security concerns (authentication, authorization, input validation). Discuss consistency guarantees and error handling.
Quick Answer: This question evaluates understanding of transactional system design, data modeling for financial domains, API design, idempotency, concurrency control, immutable audit trails, and security considerations in an online banking context.
Design a Basic Online Banking System with Safe Money Movements
Problem
Design a minimal but production-minded online banking service. The service must support four core money operations and be correct under retries and concurrency, with strong auditability and security. Correctness — not raw scale — is the focus.
The service must support:
Create accounts
for users (a balance bucket in one currency).
Deposit
funds into an account (money entering from an external rail).
Withdraw
funds from an account (money leaving to an external rail).
Transfer
funds between two internal accounts.
Beyond the happy path, your design must satisfy these properties:
Atomicity
— each money movement either fully commits or has no effect (no partial postings).
Idempotency
— a client can safely retry an operation (after a timeout or network blip) without it executing twice.
Concurrency safety
— no double-spend; two concurrent debits on an account cannot both pass a balance check the account cannot actually cover.
Immutable ledger / audit trail
— financial records are append-only; corrections are reversing entries, never edits or deletes.
Security
— authentication, authorization (callers act only on accounts they are entitled to), and input validation.
Produce: a data model (tables and key fields), an API design (endpoints, key request/response fields, idempotency contract), the transaction flows for deposit / withdrawal / transfer including concurrency control, the immutable-ledger and auditability approach, security considerations, and the consistency guarantees and error-handling strategy.
Constraints & Assumptions
Single-region
deployment, one primary relational database (PostgreSQL) with optional read replicas. The "basic" framing means one ACID primary is meant to keep correctness tractable.
Fiat currency
(e.g. USD), one currency per account. Multi-currency FX is out of scope (mention as an extension only).
Amounts are
integer minor units
(cents). All timestamps are UTC.
Out of scope: interest, statements, loans, fraud/AML scoring, KYC onboarding, and the internals of external rails (model them as an async boundary with a settlement callback).
Rough scale to justify the single-primary choice: assume on the order of ~1M active users doing a handful of operations/day (low-hundreds of writes/sec at peak) — a sizing where a single tuned primary is comfortable.
Clarifying Questions to Ask
Are accounts single-currency, and do transfers only happen between same-currency accounts? (Assume yes here.)
Do deposits/withdrawals cross a real external rail (ACH/card) with asynchronous settlement, or are they instant for the purposes of this exercise?
What does "balance" mean to the client — posted funds, or available (posted minus pending holds)?
What durability/consistency does the business require for a read right after a write (read-your-writes), versus eventually-consistent history/balance reads?
Are partial transfers, overdrafts, or negative balances ever allowed for user accounts? (Assume no overdraft.)
What is the expected retry behavior of clients on timeouts and 5xx responses?
What a Strong Answer Covers
A strong answer treats this as a correctness problem first, scale problem second and demonstrates the following dimensions (these are the areas to cover, not the answers):
Scoping & sizing
— restates functional/non-functional requirements, states assumptions, and uses a quick back-of-envelope estimate to justify a single ACID primary instead of premature sharding.
Accounting model
— a double-entry, append-only ledger; a clear sign convention; a system/clearing counterparty account; and a coherent story for posted vs. available balance (and how pending money is represented).
Data model
— concrete tables (accounts, journals, ledger entries, holds, idempotency keys) with the key columns, constraints, and indexes; an explicit statement of what is ground truth vs. a maintained cache.
API design
— the endpoints with key request/response fields; the idempotency-key contract (replay vs. in-progress vs. key-reuse); and cursor-based history pagination.
Transaction flows
— step-by-step deposit, withdrawal, and transfer including the concurrency-control mechanism (conditional update / row locks / lock ordering) and where atomicity boundaries lie.
Double-spend prevention
— the layered defenses and why each is needed; ideally an alternative considered (e.g.
SERIALIZABLE
) with its tradeoff.
Immutable ledger & auditability
— append-only enforcement, reversal-not-edit corrections, traceability metadata, and a reconciliation invariant.
Security
— authN, per-account ownership authZ (calling out IDOR), input validation, transport/at-rest protection, and least-privilege DB roles.
Consistency & error handling
— precise guarantees (strong on primary, eventual on replicas and rails) and a stable, retry-friendly error-code table.
Follow-up Questions
How would you handle a withdrawal whose external rail
settlement fails
after funds were reserved? Walk through the exact state transitions and what the user sees.
The single primary becomes a write bottleneck. How do you shard, and what breaks about cross-account transfers once accounts live on different shards?
A popular system/merchant account becomes a
hot row
under concurrent transfers. How do you relieve the contention without losing correctness?
How would you detect that the cached
balance_cents
has silently drifted from the ledger, and what do you do when it has?
Suppose the business now needs multi-currency transfers. What changes in the ledger and the transfer flow?