Why Optimistic Locking in Ledgers Isn’t Always a Good Idea
Ledgers can be “correct” in two senses.
Ledgers can be “correct” in two senses.
In one, obvious sense, ledgers are correct when they lead to correct financial statements. That’s why double-entry safety is so important.
But in a larger sense, ledgers are also correct when they only allow the right transactions to get in. Beyond matching credits and debits, a correct ledger protects itself from overdrafts, from excessive debt, and from fraudulent use. In this, more restrictive sense, ledgers are the first line of defense.
Defense against Synapse-like catastrophes.
The company filed for Chapter 11 bankruptcy protection in April 2024. Following the bankruptcy declaration, "tens of thousands of U.S. businesses and consumers" lost access to Synapse's services, leaving questions as to the location of funds. In May 2024, former FDIC Chair Jelena McWilliams, appointed as bankruptcy trustee, said there was a shortfall between Synapse’s records and those of the banks, estimated at $65 million to $96 million.
— Wikipedia entry on Synapse Financial Technologies. Retrieved 2026-08-19
I’m Alvaro Duran, and this is The Payments Engineer Playbook. You’re already subscribed to free newsletters that “teach” you how to get a job as a software engineer.
But you don’t want to get a job; you already have one. What you want is to learn how to be great at your job. Especially as a payments engineer, where stakes are sky-high, and the margin for error is razor-thin.
In The Payments Engineer Playbook, we investigate the technology that transfers money. All to help you become a smarter, more skillful, and more successful payments engineer. And we do that by cutting off one sliver of it and extracting tactics from it.
In Assume that Manual Work in a Ledger Is Impossible, we introduced the idea that, in order for reads to be consistent, passing only a timestamp wasn’t enough—the version of the Account was also required. Otherwise, newer, but backdated entries could make future reads give different values.
In this article, I’m looking at versioning from another angle: the moment new entries get stored. Ledgers can only be correct in the sense of gatekeeping if transactions get processed one by one, and versioning is one, very obvious way to do it.
It doesn’t always work, though.
Here’s what you can expect in today’s article:
What is optimistic locking for ledgers
Why engineers often choose it for authorizing ledgers
Thread contention, and a better alternative to versioning
Enough intro, let’s dive in.
How to create money out of thin air
It’s a rite of passage for payments engineers to learn how to prevent double charges in ledgers.
When two requests to withdraw $40 from a $50 bank account are received simultaneously, and no controls are in place, you run the risk of allowing both requests to be admitted.
The first thread would read a balance of $50, note that the request is for a lower amount, and proceed to withdraw the funds. The second thread, while the first hasn’t still completed, would read the same balance, and note that the request is also valid, and will proceed.
The first thread would then finish, and update the balance to now be $10. The second thread would finish a moment later, and update the balance to be $10.
Your ledger would have created $40 out of thin air.
Optimistic engineers choose locking at their own peril
The simplest way to enforce a “no negative balance” rule is to use version locking.
Let’s talk databases1 for a moment: the Accounts table has a version column, an integer that gets incremented every time an Entry is created that’s associated with the account. The Entries table, on the other hand, has a account_version, also an integer, that contains the version of the Account when the Entry was added to the ledger.
This is just a continuation of what we described in the last article. Some engineers will see this and think “oh, this is just optimistic locking”, but it isn’t: optimistic locking is scoped in the same table, whereas in this case, the locking version is stored in a different table.
A while ago, I noted that this was a genuine innovation, and as far as I know, it was first implemented by Modern Treasury.
Since account_version changes whenever an Entry is added to the ledger, the order is preserved; purists will call this monotonic.
However, the problem with using Account version locking is that, in the example above, the second request gets rejected altogether. And, depending on your own case, this could be a mistake.
That’s because if the Account is often updated, some clients will never be able to commit. This is called thread contention: in a concurrent scenario where only one of the threads can go through at the expense of the others, the system will experience high overall latency as a result of unsuccessful attempts and retries.
This is a problem for two related reasons. First, because accounts that are often updated rarely require real-time balance checks. They’re very likely pools of funds meant to receive cash from a bunch of sources and channel them to individual accounts. It is rare that some account that receives a high amount of top ups and withdrawals doesn’t have, in practice, a buffer of liquidity that prevents it to get to close to exhausting its balance.
And second, because these accounts are outnumbered by the others, and whether they’re classified as “pool of funds” isn’t in the code. Your own business or your clients have determined that this particular account is going to be the hub of their finances, but engineers may not be aware of this fact directly, and will learn about it when the locking mechanism becomes counterproductive.
Balance is not a property of an Account
There’s a tendency to see the Account table and include the balance in there to make balance queries whenever you need them.
Resist this impulse.
There are a few reasons for this, but the most important one is that whether a balance is used for reporting or for real-time checks matters. An account that requires a balance check cannot run the risk of data integrity. But an account that’s a pool of funds can perfectly do with an end-of-day balance, and that can perfectly sit on an Account table.
The point is that the use of that amount determines the data system where it gets stored.
This was it for this week in The Payments Engineer Playbook. I’ll see you next week.
P.S. I help fintech companies diagnose ledger problems before they become irreversible. If you feel that a second pair of eyes could be useful, reply to this email, and I’ll be in touch.
I’ll use relational lingo, but the approach can easily be translated to other types of databases.




