0) Problem Restatement
Capital One asked: design a secure, highly available backend for a mobile banking app. Customers log in, see accounts and balances, browse transaction history, transfer money (between their own accounts and to others), pay bills, and get notifications. The system must be correct with money, secure against fraud and account takeover, and always available.
1) Requirements
- Login with strong authentication (password/biometrics + MFA, trusted devices).
- Accounts, balances (available vs current), and transaction history with search.
- Transfers: internal (instant), external (ACH/wire, takes time), scheduled and recurring. Bill pay.
- Push notifications (large transactions, low balance, login from a new device).
- Audit trail. 99.99% availability.
2) Architecture
Architecture Diagram
flowchart LR
APP["Mobile app"] --> GW["API gateway - TLS, WAF, rate limits"]
GW --> AUTH["Auth service - MFA, device binding, sessions"]
GW --> BFF["Mobile BFF"]
BFF --> ACC["Accounts service"]
BFF --> TX["Transactions service"]
BFF --> TRF["Transfers service"]
TRF --> RISK["Fraud / risk scoring"]
TRF --> LED[("Ledger - double entry")]
TRF --> RAIL["Payment rails - ACH, wire, RTP"]
ACC --> CORE["Core banking system"]
LED --> K[("Events")]
K --> NOTIF["Notifications"]
K --> TX3) Key Parts
- Auth: biometrics unlock a device-bound key (the device is registered after MFA). Short-lived access tokens, refresh token rotation, and step-up auth for risky actions (new payee, big transfer). Detect new devices and locations.
- Accounts and balances: the core banking system is often the source of truth. We keep a read-optimized cache of balances updated by events, and label balances "available" (after pending holds) vs "current".
- Transaction history: stored in a read store (partitioned by account and date) fed by ledger and core events, with search by merchant, amount and date.
- Transfers (the correctness core):
- Validate (limits, payee, balance), and run a fraud check (amount vs history, a new payee, device risk). If risky, step-up auth or hold for review.
- Internal transfer: one DB transaction writes double-entry ledger lines (debit A, credit B) and updates balances atomically.
- External transfer: place a hold on funds, send to the payment rail (ACH/wire), and track states (
pending → sent → settled | returned). Returns release or reverse the hold. - Emit events → notifications, and history updates.
- Bill pay and scheduled transfers: a scheduler creates transfers on due dates (idempotent per schedule + date).
4) Security and Compliance
- Encryption everywhere, secrets in an HSM/KMS, and PII masked in logs.
- Rate limiting and bot detection on login, and account lockout with safe recovery.
- Every money movement and profile change is in an append-only audit log.
- Data retention per regulation. Customer notifications for sensitive changes (new device, new payee).
5) Availability
- Stateless services, multi-AZ, active-active across two regions for read paths. The ledger uses a strongly consistent database with synchronous replication within the region and a failover plan.
- If core banking is down: show cached balances ("as of"), accept transfers into a queue where allowed, and clearly show pending status.
- Pre-scale for payday and month-end peaks.
6) Wrap-Up
Put strong device-bound authentication with step-up MFA in front of a mobile BFF that talks to accounts, transactions and transfers services. Make every transfer idempotent, fraud-checked and recorded in a double-entry ledger (instantly for internal transfers, with holds and tracked states for external rails), and feed history and notifications from ledger events. Mirror core banking data into read-optimized stores, keep a full audit trail, and design for multi-region availability with graceful behavior when core systems are slow.