Hi, Aniket here 👋🏻 and welcome to my System Design Newsletter 😊!
In the previous post, we learned how to design a payment system. This post is about one of the most important pieces of a payment system, the “Ledger System” which serves the following purposes:
Maintaining accounts payable and receivable
Reconciliation
Determining revenue and profitability
Basic ledger system
Figure 1 shows the design of a basic ledger system that maintains the amount, txn_type, and merchant who has to be paid out.
Challenges
Though it’s simple, the above system poses the following challenges:
Slow settlement: Calculating the balance for a merchant requires an expensive GROUP BY aggregate for each merchant, which struggles if you have to pay out millions of payments daily (think Amazon’s scale).
Partial payments: It’s impossible to split a single payment into multiple payouts since there is a many-to-one relationship between payments and payouts. This makes for frustrating product limitations - if a merchant deemed risky makes a single $100,000 transaction, either the entire transaction has to be blocked or none of it.
Data inconsistency: Since this is just a SQL database, nothing is preventing the payouts from becoming inconsistent. The payout_id can be ensured to be a valid foreign key, but nothing is stopping it from being nulled out.
Double-entry accounting
Enter Double-entry accounting. It is a battle-tested approach to modeling financials. Here’s how:
It forces you to state not just what financial state change occurred, but why.
This is done by maintaining Books and Journal Entries:
Each book maintains it’s own set of CREDIT/DEBIT transactions.
Each transaction should have at least two journal entries in the ledger system (hence the name double-entry) where every entry must have a source account and a target account.
According to the accounting equation, every journal entry must balance to 0, so each cent lost is matched with a cent gained.
Figure 2 shows “merchant_book” and “customer_account_book” for the example we took in Figure 1.
The first $100 transaction has two entries: a CREDIT entry in merchant_book and a corresponding DEBIT entry in customer_account_book, under the same Journal ID J01.
Account balances are maintained separately in account_balance table for quick settlements. This solves the GROUP BY query challenge mentioned earlier.
This is how a ledger system is designed, only a real ledger is more complicated.
Applying these strict accounting rules makes reconciliation much easier!
Resources:
https://developer.squareup.com/blog/books-an-immutable-double-entry-accounting-database-service.