Discover our latest AI-powered innovations around faster payments, smarter workflows, and real-time visibility.Learn more →
Accounting For Developers, Part II
In this second part of our two-part series, we build a ledger for a Venmo clone by applying the accounting principles we learned in Part I.
Introduction
Welcome back to our Accounting for Developers series. Before diving into this post, you may want to read Part I here.
In this tutorial, we will be designing the ledger for a Venmo clone—a simple digital wallet app. Throughout, we will show how to apply the double-entry accounting principles we covered in Part I. We hope to publish guides about more complex use cases (lending, insurance, etc.) in the future.
If you’re curious about the API calls and system design considerations of designing a digital wallet app, you can also check out our previous journal on how to build a digital wallet.
To recap, for a system to gain the consistency benefits that accounting provides:
- It should be composed of accounts and transactions:
- Accounts should be classified as debit or credit normal;
- Transactions should enforce double-entry upon creation. Each transaction needs to have at least two entries, which, in aggregate, must affect credit and debit sides in equal amounts.
- The aggregate balance of credit normal accounts and debit normal accounts should net out to zero (credits = debits).
Building a Venmo clone
Step 1: Reviewing the use case
Let’s start with the product requirements of our Venmo clone, first from the user’s perspective:
- Each user will have an account balance, exposed via the app
- Users can add to their balance by way of card payments
- Users can send money to each other in the app
- Users can withdraw their balance into a bank account via ACH or a debit card
- Users will pay a small fee when they make a withdrawal from the app, to be deducted from their wallet balance.
From a product perspective:
- We want to discern between the account balances for each user and expose them to said users consistently;
- We want to ensure cash in hand in our bank account is always equal to the total users deposited in the app;
- We want to properly calculate and collect revenue from fees;
- Each deposit will need to account for a 3% card transaction processing fee paid by us.
Step 2: Building your chart of accounts
With these requirements in mind, let’s map our chart of accounts (COA). The COA is a simple depiction of the accounts we will need, their type, and normality:
Let’s review this in detail:
A cash account represents the amount of money we are holding in our bank account in cash. Because it represents an asset or use of funds, we will treat it as a debit normal account. For more information on debit and credit normal accounts, refer to the “Dual Aspect” section of Part I of this series here.
The user balance accounts represent funds we are holding on behalf of our users. Because users should be able to withdraw them at any time, they are funds we ‘owe’—or liabilities. Those funds are technically now available for our ‘use’ - and as such, they are sources of funds. Therefore, they should be credit normal accounts. Notice that we need one account for each customer that creates an account with us.
To track card fees, we will be using a debit normal account. This account’s balance will increase every time we pay off card processing fees. This is a debit normal account because it represents expenses or uses of funds.
Finally, the fees we collect in each transaction are to be treated as revenue. Given these are sources of funds, they are credit normal accounts.
Step 3: Mapping sample transactions
After mapping our chart of accounts, we should consider the typical events that will affect the ledger. For the sake of this example, we will cover three transaction types:
- Transfers: user sends money from their balance to another user.
- Deposits: user adds cash into their account balance. At the time of transfer, we need to account for the credit card processing fee. (Let’s assume, for the sake of this example, that credit card fees are paid by us.)
- Withdrawals: user withdraws from their account balance. We charge a fee when users withdraw from the app, deducted from their balance. At the time of transfer, we need to account for our own service fee as revenue.
Let’s walk through the implementation for these transactions, starting with a transfer:
This chart shows a typical transfer of $100 from Art to Brittany. In this case, the transaction amount is debited (deducted) from Art’s Wallet (who’s initiating the transfer) and credited (added) to Brittany’s Wallet (who’s the receiver).
Note that this logic can be used for any in-app transfer—we just have to designate which wallet is the sender vs receiver in each case. All wallets are represented as credit normal accounts. If Brittany was sending money to Art, then Brittany’s balance would be debited (decrease), and Art’s balance would be credited (increase).
Next, let's look at a deposit:
In this model, three accounts are involved: the Art's Wallet, Cash, and Card Processing Expenses. When Art deposits an amount into his wallet, he will see the balance increase by the same amount. Simultaneously this will increase cash balance and the total paid in processing fees.
To further illustrate this, let’s say Art deposits $300 in his wallet balance using a credit card. Recall that for the sake of this example, our app is paying for card fees. To counterbalance the $300 credit (increase) on Art’s Wallet, we need two debit entries: one on the cash account (which increases it) and one on the card processing expenses account (which also increases it).
Our card processing expenses account increases by $6 (or 2% of the transaction). And given we are recording this expense as paid off to our credit card vendor, our cash balance increases by $294 ($300-$6).
The power of double-entry is recording this flow of money in a single event. Without double-entry, we would need a way for the system to recognize all of the deposit transactions and properly account for card fees. By recording all of the money movement in a single transaction with multiple entries, we make sure our system is consistent. As debits = credits, money in equals money out.
The same goes for a withdrawal:
A withdrawal is similar to a deposit, except that in this case, we are charging an extra fee from the user and recognizing it as revenue from fees. This transaction will decrease Brittany’s Wallet and Cash but will increase Revenue from Fees.
For example, let’s say Brittany is withdrawing $500 from her wallet balance. Brittany knows that she will pay a fee on that transfer amount. Let’s assume that the fee is 0.5% of the withdrawal amount, or $2.50. Her user wallet gets deducted for the entire $500 + $2.50, or $502.50. That is the debit entry (decrease) on her user wallet balance.
To represent this on the credits side, we will add a credit entry that deducts the cash account for $500, given this is actual money we wired out to Brittany. However, we owe $2.50 less to Brittany and can recognize the fees we charged from her as revenue by crediting (increasing) our revenue from fees account.
There are many different ways to model this. We could have chosen to have Brittany receive $497.50 ($500-$2.50), for example. In this case, we would add/credit the $2.50 we kept to revenue from fees similarly, but our cash would only decrease/credit by $497.50. The ledger would still balance. Thinking in terms of credit and debit normality gives you the flexibility to log transactions in the best way for your business.
Step 4: Bringing it all together
Let’s review the logical elements we would need to create to service this use case:
- One ledger object that represents the entire collection of accounts and transactions. All of our accounts and transactions should belong to a single ledger.
- At least four types of account objects:
- User Wallets (one per user, credit normal)
- Cash (single account, debit normal)
- Revenue from Fees (single account, credit normal)
- Card Processing Expenses (single account, debit normal)
- At least three modeled transactions
- User Transfer
- Deposit
- Withdrawal
If you are building a ledger using a relational database you’ll want to model accounts as belonging to a single ledger where transactions (or events) will be written into. Accounts should have constraints according to their type: credit or debit normal. Such constraints should dictate how debit or credit entries affect account balances according to the principles we covered on Part I.
Similarly, transactions would have to be modeled in a way such that they are composed of at least two entries. Such entries would have a ‘direction’, one of debit or credit. Your system should enforce equality between the sum of amounts on debit entries and the sum of amounts on credit entries.
The double-entry treatment for each type of transaction we covered on step 3 can then be mapped into functions in your application code that dictate how you will write into the ledger database as transactions happen.
By setting up the ledger as a double-entry system, we ensure that our Venmo clone scales consistently. And as new product requirements come up or functionalities are rolled out, we can update our chart of accounts and the transaction models to represent them in the ledger appropriately.
All that said, it can be onerous for generic databases to reliably handle double-entry accounting. If you are a developer who works with money, the opportunity cost of building a ledger from scratch may not be worth it. Modern Treasury Ledgers simplifies the process of building a dependable double-entry system. Reach out to us to learn more.
Home-grown ledgers break
Discover how, why, and what to do in this journal.
Subscribe to Journal updates
Discover product features and get primers on the payments industry.