Project Case Study: Designing Stripe’s Ledger System
Recording transactions between millions of accounts is not a matter of simple arithmetic. It requires a system that is 100% auditable, ACID-compliant, and perfectly consistent.
1. Requirements
Functional:
- Immutable Audit Trail: Every cent moved must be recorded forever.
- Precision: 100% accuracy (no rounding errors).
- Global Support: Transactions spanning multiple regions.
Non-Functional:
- Transactional Integrity (ACID): Atomic updates for all account movements.
- High Throughput: Thousands of ledger entries per second.
- Availability: 99.999% uptime.
2. High-Level Design (HLD)
Stripe’s ledger doesn't just store a "Balance" field. It uses Double-Entry Bookkeeping.
- Every transaction creates two entries: a Debit and a Credit.
Entriestable:id, account_id, amount, currency, timestamp, transaction_id.- The true balance of an account is always:
SUM(Entries.amount) where account_id = X.
3. Low-Level Design (LLD): Transactional Integrity
We use the Transactional Outbox Pattern to ensure we don't have "Dual Write" problems between our Database and our Event Bus (Kafka).
- Start SQL Transaction.
- Write Ledger Entries: Insert the Debit/Credit rows.
- Write to Outbox Table: Store the event that a payment happened.
- Commit Transaction.
- CDC (Change Data Capture): A process like Debezium polls the Outbox table and pushes events to Kafka for downstream consumption.
4. Scaling Challenges: Sharding
We shard the Entries table by account_id using Consistent Hashing. This ensures that all entries for a specific account live on the same physical database node, allowing for fast, atomic updates to that account's state.
Summary
Designing a ledger is about Immutability. By treating the ledger as an append-only stream of entries and using the Outbox pattern for integration, you can build a financial system that scales to global volumes with total confidence in its accuracy.
Next: Designing a Distributed Message Queue Previous: Beyond CAP: The PACELC Theorem
