Journal

Accounting For Developers, Part II: Ledgering for a Wallet App

In this second post of our three-part series, we build a ledger for a Venmo clone by applying the accounting principles we learned in Part I.

Image of Lucas Rocha
Lucas RochaProduct Manager

Updated on August 14, 2025.

Introduction

Welcome back to our Accounting for Developers series. If you missed the first part, we recommend starting with Part I, where we cover accounting foundations.

In this guide, we will walk through how to design the ledger for a Venmo-style digital wallet app. You'll see how to apply the double-entry accounting principles as we model user transfers, deposits, and withdrawals. We will also share how to structure this using a relational database.

If you’re curious about the API calls and system design considerations of designing a digital wallet app, you can also check out our guide on how to build a digital wallet.

Why a Double-Entry Ledger is Essential for Wallet Apps

To gain consistency, transparency, and correctness in a financial system, your architecture should:

  • Model money flow using accounts and transactions
  • Classify accounts as either debit normal or credit normal
  • Enforce double-entry on every transaction (at least one debit + one credit)
  • Ensure that total debits = credits across all entries (the aggregate balance of credit normal and debit normal accounts should net to zero)

This structure makes your system auditable, scalable, and resistant to bugs in payment flow.

Designing a Ledger, Step 1: Define Product Requirements for the Wallet App

Let’s begin with what users should be able to do if the app works correctly:

  • View their wallet account balance
  • Add funds to their balance via card or bank payments
  • Send money to (and receive money from) other users in the app
  • Withdraw their balance into a bank account via ACH or instant payment
  • Pay a small fee when they make a withdrawal from the app, to be deducted from their wallet balance

From a product perspective, we also want to:

  • Distinguish user-specific balances and expose them to said users consistently and accurately
  • Ensure the sum of all user balances equals the cash in our bank account
  • Properly calculate and collect revenue from fees
  • Account for a 3% card transaction processing fee for each deposit, paid by us

Step 2: Designing the 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:

Sample Chart of Accounts - Venmo Clone
A simplified version of our wallet app's Chart of Accounts

To review:

  • Cash represents funds we actually hold in our bank account. Because it represents an asset or use of funds, it's a debit normal account.
  • User's Wallet Balance represents funds we hold 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," meaning they are sources of funds, and thus credit normal accounts.
    • We need one User Balance account for each customer that creates an account with us.
  • Card Processing Fees represent expenses or uses of funds; therefore, this is a debit normal account. This account’s balance will increase every time we pay off fees.
  • Revenue from fees we collect in each transaction are sources of funds, so they are credit normal accounts.

Step 3: Modeling Key Transactions

We should consider the typical events that will affect the ledger. For the sake of this example, we will model three core transaction types:

  1. Transfers: User A sends money from their balance to User B.
  2. Deposits: User A 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.)
  3. Withdrawals: User B 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.

Example 1. A Transfer

Venmo Clone Transfer Map
Mapped funds transfer of $100 from Art to Brittany

This example shows Art transferring $100 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 initiating and which is receiving in each case. As marked in our COA above, User Wallets are 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).

Example 2. A Deposit

Art's Deposit
Art's Deposit of $300

In this model, three accounts are involved: Art's Wallet, Cash, and Card Processing Expenses(recall that for the sake of this example, our app is paying for card fees).

  • Art deposits $300 in his wallet balance using a credit card.
  • To counterbalance the $300 credit (increase) on Art’s Wallet, we need two debit entries:
    • One on the card processing fees account (increases by $6, or 2% of the transaction)
    • One on the cash account. Given we are recording this expense as paid off to our credit card vendor, our cash balance increases by $294 ($300-$6).

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 event (in this case, a deposit) with multiple entries, we make sure our system is consistent. As debits equal credits, money in equals money out.

Example 3. A Withdrawal

Brittany's Withdrawal
Brittany's Withdrawal of $500

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.

  • Brittany withdraws $500 from her wallet balance (Brittany knows that she will pay a fee).
  • Let’s assume that the fee is 0.5% of the withdrawal amount, or $2.50.
  • Her user wallet gets deducted for $500 + $2.50, or $502.50
  • We need to wire Brittany her money, so we add a credit entry to deduct our Cash account; however, we owe $2.50 less to Brittany, a small amount we can recognize as Revenue.

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: Database Modeling and Application Logic

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

In dev terms:

  • Accounts table fields: id, user_id, account_type, normality
  • Transactions table fields: id, timestamp, description
  • Entries table fields: id, transaction_id, account_id, amount, direction
  • For entries, enforce: sum of debit amounts == sum of credit amounts, per transaction
  • For each transaction type, create logic that writes 2+ entries with totals that net to zero (determine entry direction based on account type)

Conclusion: Why Ledgers are Worth Building Right

By setting up the ledger as a double-entry system, we ensure that our Wallet App:

  • Scales without drift or inconsistency
  • Produces accurate balances and reconciliation data
  • Extends to new features with minimal refactoring (as new product requirements come up or functionalities are rolled out, we can update our COA and the transaction models to represent them in the ledger appropriately)

Up next: Go on to Part III of this series, where we apply these principles to build a lending marketplace.

And remember, whatever you're building, you don't have to do it alone. Implementing a robust ledger from scratch is non-trivial, and it can be onerous for generic databases to reliably handle double-entry accounting. If you're developing a product that moves money, the opportunity cost of doing this in house can be high.

Modern Treasury Ledgers provides a production-ready, developer-friendly, double-entry ledger. Reach out to us to learn more.

Authors
Image of Lucas Rocha
Lucas RochaProduct Manager

Lucas Rocha currently is the PM on the Ledgers product, driving strategy for the company’s database for money movement. Before Modern Treasury, Lucas worked in VC at JetBlue Technology Ventures and Unshackled Ventures. He earned his MBA from Harvard Business School and his bachelor’s degree from Northeastern University.

Home-grown ledgers break

Discover how, why, and what to do in this journal.

Read the article