A ledger that works for 1,000 transactions per day and a ledger that works for 1 million transactions per second are built differently. The double-entry model tells you what to store. Ledger architecture tells you how to store it at scale while keeping balance queries fast and consistency guarantees intact.

The Balance Computation Problem#

Deriving balance as SELECT SUM(amount) FROM ledger WHERE account_id = X is correct but slow when the ledger has billions of rows. An account active for 10 years has millions of entries. Full scan on every balance check is unacceptable.

Solution: materialized balance with event sourcing. Maintain a balances table with the current balance per account. On every ledger write, update the balance atomically in the same transaction. The ledger is the source of truth; the balance table is a cache. If the balance table corrupts, you recompute from the ledger.

Checkpointing: periodically create a balance snapshot as of a specific ledger sequence number. Balance as of now = snapshot balance + sum of entries since snapshot. This bounds the recomputation cost.

graph TD A[New transaction: debit Alice $50] --> B[Insert ledger entry: sequence=10042] B --> C[UPDATE balances SET amount=amount-50 WHERE account=Alice] B --> D[INSERT ledger entry: credit Bob $50, sequence=10043] D --> E[UPDATE balances SET amount=amount+50 WHERE account=Bob] F[Balance query: Alice] --> G{Use balance table} G --> H[Return: instant lookup from balances table] I[Audit: recompute Alice balance] --> J[SUM ledger entries since last snapshot] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style H fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style I fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style J fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Sharding the Ledger#

Shard by account ID. All ledger entries for account X go to the same shard, which keeps per-account queries fast. Cross-account transfers (Alice to Bob on different shards) require a distributed transaction: write both entries atomically across shards.

For high-throughput systems, use a multi-currency or multi-asset account structure where each asset type is a separate account. This distributes write load: a user holding BTC, ETH, and USD has three accounts on potentially three shards.

Sequence Numbers and Ordering#

Each ledger entry needs a monotonically increasing sequence number per account for ordering. Use a database sequence or a distributed ID generator that embeds a timestamp. Sequence numbers let you reconstruct history deterministically and detect missing entries (a gap in sequence numbers indicates a lost write).

At Salesforce#

The billing ledger at Salesforce partitioned entries by org ID. Each org’s billing history lived on one shard, making per-org balance queries single-shard. Cross-org adjustments (rare, for enterprise agreements spanning multiple orgs) went through a separate reconciliation service that wrote compensating entries on each shard. Annual audit queries against 5 years of billing history ran in under 2 seconds because the snapshot + delta pattern limited the scan to entries since the last monthly snapshot.

What I’m Learning#

Ledger architecture is a balance between write throughput (favor append-only, sharded ledger) and read performance (favor materialized balances, checkpoints). Getting both requires treating the ledger as immutable truth and the balance table as a derived cache.

What’s the largest ledger you’ve worked with, and how did you handle the balance query performance problem?