At Modern Treasury, we’ve designed our Ledgers API to flexibly track any type of money movement. This makes it particularly easy to adapt ledgers to many of our customer use cases, like helping ClassPass track studio payouts when customers attend classes. We’ve been particularly intentional that we never violate three core principles for our ledger system: double-entry, auditability, and immutability. Oftentimes, we’ll get the question: why immutability? In this blog post we’ll discuss the importance of immutability in a ledger system.
Fallacies with Mutability
When engineers design their first MVP, it’s not surprising that accounting systems and principles tend to be an afterthought.
Imagine designing a marketplace with merchants and consumers where consumers can place orders from merchants. This marketplace collects payment from consumers immediately and disburses these in a weekly payout to the merchant. The MVP might contain an Orders table. Once a week a cron job is run to calculate last week’s orders and generate a payout to the merchant.
The Orders table is effectively your “ledger” and tracks how much money has been collected and will be paid out. This would work in a simple world. However, the real world is much more complex. Order adjustments, fraudulent credit card transactions, returns, and refunds can all change the final amount your platform collects for an order.
It would be simple to modify the amount in the Orders table. However, this then leads to many downstream accounting problems. What if an adjustment happened two weeks later? The payout you've made to the merchant is now incorrect and you have no way to attribute the adjustment to the original payout. Equally difficult is tracking down the discrepancy in the payment to the merchant. Because the payout is the sum of many orders, your accounting team would need to dig through your list of orders to find the offending order. Imagine if a merchant had thousands of orders and complained about a $10 discrepancy in their $10,000 payout. It could take days to track this down.
In a double-entry system, accountants can generally reverse-engineer the discrepancies that have happened. However, if the data has been mutated, then the data is irreversibly destroyed and then becomes impossible to figure out what changed.
In the above example, it’s important to isolate business-level objects (e.g. Orders) from your accounting system. The business-level objects are a mutable presentation to your end-user whereas the accounting-level objects represent trackable money movement and the ultimate “source of truth.”
Let’s imagine you have a table called “Ledger Transactions” that records accounting-level entries. When an Order is created, the system should create a Ledger Transaction to represent the money collected by you but owed to the merchant.
If order adjustments are made the ledger transaction stays immutable. There are two approaches here:
- Completely reverse the ledger transaction by creating a new ledger transaction with the opposite amount. Then, then create a new ledger transaction with the new correct amount
- Calculate the difference between the ledger transaction’s new correct amount, and create a new ledger transaction that sums the two to the correct amount
In the above multiple Ledger Transactions can point to one Order. It becomes particularly easy to pull up all the ledger transactions tied to an Order. The sum of all ledger transactions should equal the mutable Order.amount column, but of course this may not be the case for all systems (e.g. if the platform takes a cut of the payment for facilitating the ledger transaction).
This functionality becomes crucial if an adjustment is made after a weekly Payout. An order can then technically be paid out in more than one payout. This conceptually makes sense—if an adjustment is made in the following week it should not affect a prior week’s payout.
With immutability enforced your product will have a paper trail and the above situation becomes significantly easier to figure out discrepancies.
Why does Ledger Transaction have a pending and posted state?
In our ledger system, we have a status column on Ledger Transactions. This status can be either pending or posted. A ledger transaction is mutable while pending and immutable once posted.
Payments, like ACH payments or wires, go through a complex flow in the banking system to transition from in-progress to completed. Ledger transactions need to track these state transitions. For the merchant’s weekly ACH payout, for example, the ledger transaction begins in the pending state. The ledger transaction transitions to posted if completed, but if any issues arise, we can alternatively mark the ledger transaction as archived. The reason for this is that most bank payments are not instant: they take up to three days to post at which point they may still fail. This process closely mirrors what you see in your bank account statement, which is at its core a ledger. We’ve outlined how these statuses are mapped here.
You might wonder why we don’t just make everything immutable. Why wouldn’t we simply create a new reversed ledger transaction if a payment fails? This is a different but valid design as well, but leaves a messier paper trail when honest mistakes happen. Imagine if your keyboard didn’t have a backspace key and mistakes had to be corrected with a reversal. We simply chose our design to reflect exactly what is happening in the bank rails. Of course the pending state may not make sense for all products, and it’s up to the discretion of the developer to decide when this is appropriate.
Immutability in a double-entry ledger system is one of the core defining principles to adhere to good engineering and accounting principles. By using Modern Treasury’s double-entry ledger we enforce immutability in a way that your current and future finance and accounting team will thank you.