Subscribe to get high-signal insights on how modern fintech is built.

engineering

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.

By Alex Kugell ·

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

Frequently Asked Questions

Why is SUM too slow for ledger balance reads at scale?
A balance derived from SUM scans every entry an account has ever had. A new account with 50 entries returns in milliseconds, but a three-year-old business account with 200,000 entries takes long enough to blow the latency budget for a prepaid card authorization. The scan cost grows linearly with account age, which makes read latency unpredictable.
How do balance deltas differ from balance snapshots in a ledger?
Snapshots store the full balance at a point in time, so reads only sum entries since the snapshot. But a backdated correction invalidates every snapshot after its timestamp. Balance deltas store changes at hour, day, month, and year granularity. A read composes the relevant deltas with a fixed number of lookups regardless of account age, and a backdated entry only updates the four deltas for its own timestamp.
Why does one ledger entry trigger 80 balance updates?
Each entry updates deltas at four time granularities (hour, day, month, year) for the account and every parent account in the hierarchy. With an average nesting depth of five levels and four lines per entry, the multiplication produces roughly 80 updates. At 10,000 entries per second, that's 800,000 balance updates per second, which requires an asynchronous coalescing pipeline to avoid write contention.
Can a financial ledger use eventual consistency for balance reads?
It depends on the query. A prepaid card authorization needs strong consistency because a stale balance could approve a transaction the account can't cover. An operations team pulling yesterday's closing balances for reconciliation can tolerate 10 seconds of staleness. Fragment runs both models simultaneously and lets you choose per account at the schema level.
When should a fintech use Postgres vs DynamoDB for a ledger?
At moderate volumes, a few hundred entries per second, Postgres with periodic snapshots handles balance reads, tamper evidence, and ad-hoc SQL without extra infrastructure. The ceiling arrives at hot accounts like omnibus wallets where serializable isolation creates write contention. DynamoDB trades away ad-hoc SQL and JOINs for single-digit-millisecond reads at any scale and conditional writes for double-spend prevention.

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.

Keep reading

anatomyFragment Went from Ledger API to Financial Control PlaneFragment sold fintech engineers a better ledger, and two years later Ramp calls it the control plane for its coding agents.
analysisBridge Is Stripe's Stablecoin FactoryTwenty months after the $1.1 billion acquisition, Bridge makes stablecoins for MoneyGram, MetaMask, and anyone else with a brand to put on a dollar.
engineeringFail-Closed Is the Only Safe Default in Banking Event SystemsEvent-driven architectures default to skip-and-continue, but in payments, every skipped event is a check that didn't run before money moved.
View more ›