Advancing the Ledger
The ledger data model is settled, but making balance reads fast at 10,000 entries per second is an active computer science problem.
Your ledger works. Double-entry, append-only, balance derived from the sum of entries. At 100 transactions per minute, SUM(amount) WHERE account_id = ? returns in single-digit milliseconds. The data model is sound.
Now run that same query on a three-year-old business account with 200,000 entries. Or run it on a platform omnibus account that accumulates 10,000 new entries per second. The SUM still returns the correct answer, but it takes long enough that the prepaid card authorization waiting on it has already timed out.
The data model that makes a ledger correct is the same one that makes it slow at scale. The balance must be derived from entries, not stored independently. But deriving it from scratch on every read stops working when the entry count is unbounded and the latency budget is 30 milliseconds.
This is the performance engineering problem hiding inside every production ledger. The invariants are settled. The computation that honors them at scale is still advancing.
The SUM ceiling
The standard fix for slow balance reads is a snapshot. Periodically compute and store the total. On read, load the most recent snapshot and sum only the entries posted since. Instead of scanning 200,000 rows, you scan the last 500.
This works until someone posts a backdated entry. A correction dated to last month invalidates every snapshot after its timestamp. You either recompute all affected snapshots, which is expensive, or accept that snapshots can be temporarily wrong, which is dangerous in a financial system because any query during that window returns a number that doesn't match the entries.
Snapshots also add a reconciliation surface. The snapshot says $42,300. The entries sum to $42,150. The entries are right, always. But now you have a discrepancy to investigate, and the investigation takes engineer hours.
A restaurant that posts a running total on the wall after every shift can tell you the day's revenue in a glance. But if a server discovers a missed receipt from lunch and adds it to the afternoon total, the wall number and the receipts don't match until someone recounts everything since lunch.
Balance deltas
Fragment took a different approach. Instead of snapshots at arbitrary intervals, they store balance deltas at fixed time granularities: hour, day, month, and year.
When a ledger entry posts, the system updates the delta for its hour, its day, its month, and its year. Four updates, regardless of when the entry is dated. A correction from last March costs the same as an entry from right now.
Reading a historical balance means composing the right deltas. "What was the balance at 2:47 PM on March 12?" reads the year total through February, the March total through March 11, the March 12 total through the 1 PM hour, and the 2 PM hour's delta through 2:47. A fixed number of reads whether the account is three days old or three years old.
In production, this yields 30-millisecond transactional balance reads and 130-millisecond historical reads. Those numbers hold whether the account has 50 entries or 500,000, but the cost of maintaining them lands on the write side.
Eighty writes per entry
A single ledger entry triggers roughly 80 balance updates. That number breaks down as 4 granularity levels multiplied by an average hierarchy depth of 5 (an account nested inside parent accounts) multiplied by 4 lines per entry. Every entry updates its own account's deltas and the deltas of every parent account above it.
At 10,000 entries per second, that's 800,000 balance updates per second. No database handles that as synchronous writes without contention destroying throughput.
Fragment's solution is an event-processing pipeline. The entry writes to DynamoDB. A change-data-capture stream (DynamoDB Streams) feeds EventBridge, which routes to SQS queues. At each cascade level, the system batches incoming events and coalesces their balance effects.
Instead of 80 individual updates, the pipeline folds overlapping hour deltas into one write, day deltas into another, and so on up the granularity stack.
At 10,000 entries per second, the eventually consistent balance has a p95 staleness of 10 seconds. The balance is always correct, but it might be 10 seconds behind the latest entry. An auditor reviewing end-of-day positions won't notice that lag, but a prepaid card authorization reading a balance 10 seconds late could approve a transaction the account can't cover.
Which is why the system runs two consistency models at once.
Two questions, two consistency models
"Can this account afford this debit right now?" and "what was this account's balance last Tuesday?" are different questions with different correctness requirements.
The spending question needs strong consistency. A prepaid card authorization can't run against a stale number. Fragment handles this by synchronously updating a transactional balance on every entry post. The read is 30 milliseconds and reflects every entry that has been committed.
The historical question can tolerate eventual consistency. The operations team pulling yesterday's balances for reconciliation doesn't care whether the number was 10 seconds behind real time when it was computed. They care that it converges to the correct total, which it does.
Applying strong consistency to every account caps throughput because serializing writes on hot accounts creates contention, while applying eventual consistency everywhere leaves balance checks unreliable for real-time authorization.
Fragment's two-tier approach lets you choose per account. Fee collection accounts with high write volume run eventually consistent, and customer-facing prepaid accounts run strongly consistent. The same ledger, the same entries, two balance-computation paths selected at the schema level.
Entry conditions tie directly into this. A precondition that says "reject this debit if the balance would go below zero" requires strong consistency on the account it references. The condition evaluates atomically with the entry post, so it can't run against a number that might be 10 seconds stale.
The Postgres question
Most fintech ledgers run on Postgres, and for good reason. Serializable transactions, SUM queries, hash chaining for tamper evidence, and ad-hoc SQL for debugging. At moderate volumes, a few hundred entries per second, Postgres with periodic snapshots handles everything described above without the complexity of an event-processing pipeline.
The ceiling arrives at hot accounts. The omnibus wallet that every transaction touches, the fee collection account that accumulates thousands of entries per hour. Serializable isolation on those rows turns concurrent writes into a queue. One independent benchmark found Postgres throughput dropped measurably as write skew on hot accounts increased.
Fragment chose DynamoDB for single-digit-millisecond reads at any scale and conditional writes for double-spend prevention. The tradeoff is real. No ad-hoc SQL or JOINs, and consistency management falls to the application layer. Every query the operations team might run against Postgres with a one-liner instead requires a purpose-built read path.
At the other end of the spectrum, TigerBeetle is a purpose-built database for financial transactions, designed from scratch for batch throughput under strict serializability. A 2025 Jepsen audit confirmed its strict-serializability guarantees, finding one correctness bug in the query engine but no safety errors in the core ledger. But at lower volumes, Postgres remains the pragmatic answer.
The choice depends on your throughput budget. Tens of entries per second, Postgres with snapshots. Thousands per second with real-time balance reads, you're choosing between DynamoDB-style eventual consistency with a coalescing pipeline and a purpose-built ledger engine.
The invariants from the companion piece haven't changed in 700 years. The performance engineering required to honor them at 10,000 entries per second, with 30-millisecond reads and 80 balance updates per entry, is where the computer science lives.
Sources
- Building Balances I: Low-latency reads - Fragment engineering blog on deriving balances from delta aggregation instead of full scans, May 2025
- Building Balances II: High-throughput writes - Fragment engineering blog on coalescing 800,000 balance updates per second through asynchronous pipelines, June 2025
- Configure Consistency - Fragment documentation on choosing strong or eventual consistency per account at the schema level
- TigerBeetle vs PostgreSQL Performance - SoftwareMill benchmark comparing throughput and latency for financial transaction workloads
- TigerBeetle: a database built for financial transactions - Overview including reference to 2025 Jepsen audit of TigerBeetle's consistency guarantees
Frequently Asked Questions
Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.
Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.