Double-entry accounting for software engineers

Recently I've been reading the fantastic Bits about Money series by Patrick McKenzie. He gives a great insight into the world of payments. It got me thinking about payments and how it connects with my profession: accounting and software. Just like Patrick nerds out on payments, I nerd out on accounting in software, so I want to share some of my experience. Let's get started!

Most software engineering won't be dealing with accounting regularly. The accounting department handles the books at the end of the year. It's not something software engineers are involved in on a day-to-day basis unless accounting needs some numbers from you. Most software engineers I know also do not understand the principles of double-entry accounting. I believe engineers should learn double-entry accounting as a database design pattern, just like most engineers know the MVC and Decorator design patterns.

In the online world, many software handles financial transactions. In addition, with the rise of payment platforms like Stripe, more software offers transactions to their clients. However, building a fault-tolerant software system is challenging without knowledge about double-entry bookkeeping and ledgers.

A balanced history

Double-entry bookkeeping originated in the 13th century in Italy. With the rise of trade and moneylenders, they needed a system to keep their books. These were large books, probably explaining why the Dutch still call a ledger a "Grootboek" (Large Book).

Ledger of the city of Delft in 1610

The fundament of double-entry bookkeeping is an ever-present balance. When you receive $100, there needs to be some counter-effect that explains these $100. This built-in check ensures you never create or lose money during calculations.

All modern accounting software uses the double-entry pattern to implement its accounting features. My own business Moneybird does the same. So I learned to value the design pattern by building accounting software.

Transactions without double-entry bookkeeping

Let's start with an example without double-entry bookkeeping. Imagine we have a webshop with orders. Orders need payments, so you connect with a Payment Service Provider. In a simple scenario, this works perfectly: you receive an order of $100, start a payment, the user pays $100, and the order is shipped. You can model this in an Order table with total_amount, total_paid and state columns.

During payment, total_paid is increased, and when both amounts match, the state becomes paid. Everything seems fine. Until the real world introduces its quirks:

  • The first payment can fail initially but come through later. But the customer already made a second payment with a wire transfer, which you will receive days later.
  • Electronic payments are predictable in terms of amount and traceability. Wire transfers are not: people make mistakes in amounts and references.
  • Orders can get refunds, sometimes full, sometimes partial. Some chargebacks initiate from your software; some chargebacks initiate with the customer's bank or credit card.

You soon end up with an extra table  Payment, which has a one-to-many relation with Order. Now you can store multiple payment results, each with its amount. For the business logic, this is perfect. But it introduces a host of new issues.

Knowing how the payments add up might not be essential from a software standpoint. But from a business and accounting perspective, it is crucial. You don't want a system that marks orders as paid but doesn't react on refunds. You will probably lose money. But also, the small amounts can accumulate fast when the volume rises. If every transaction loses a few cents, for example, due to rounding errors, your 1M transactions per year business loses $10.000.

At Moneybird, we run a tightly controlled, double-entry bookkeeping business. That's when you learn some partners don't. At the end of the year, the balance of your internal accounts should match the balance of external accounts. Banks luckily hold tidy ledgers for their clients and mainly report correctly. Payment Service Providers are different. We have a yearly gap of roughly 1% of our revenue that we can't match with external partners. When the numbers start to grow, these gaps grow more extensive, and you lose serious money. Time for a watertight architecture for your transactions!

Refactor to double-entry accounting

A system that prints money might seem magical, sooner or later it will cause trouble. Designing your software with double-entry accounting in mind is a best practice. Not only prevents it errors, it also is a better match with the accounting department and external reporting.

First, let me introduce you to some important terms:

  • Ledger: you can compare this to a database. Every business has one ledger for itself internally. You can also have external ledgers. A bank account is an example of an external ledger.
  • Ledger account: Ledger accounts categorize the money flowing through the ledger. It is modelled like a tree, with the top most levels pointing to the balance sheet or profit-loss statement. The second level points to items on the reports. The lower levels are customizable. Examples of common ledger accounts are revenue, bank accounts, accounts receivable, and accounts payable.
  • Journal entry: this is a single record in the ledger, comparable to a row in a table. It contains the actual money movement between ledger accounts.
  • Financial fact: although this is not a commonly used term, I like to use financial facts to make it easier to reason about the external world connected to the ledger. Financial facts are events in the world reflected in the ledger. Mainly they cause money movement, but also movements in assets or liabilities in a business are financial facts. The journal entries of a financial fact should always be balanced.

Now, let's journalize the order example. In double-entry bookkeeping, a small table illustrate the journal entries. I use an extended notation for ledger accounts to clarify where the ledger accounts are in the tree — more on this in the next issue.

╭──────────────────────────────────────────────────┬───────┬────────╮
│ Ledger account                                   │ Debit │ Credit │
├──────────────────────────────────────────────────┼───────┼────────┤
│ balance_sheet.current_assets.accounts_receivable │ 100.0 │    0.0 │
│ profit_loss.revenue.general                      │   0.0 │  100.0 │
├──────────────────────────────────────────────────┼───────┼────────┤
│                                                  │ 100.0 │  100.0 │
╰──────────────────────────────────────────────────┴───────┴────────╯
Order journal entries

In these journal entries we reported $100 revenue to the general revenue ledger account. To balance this action, we increased the accounts receivable, because we want the customer to pay for this order.

All journal entries are dated, so we can always calculate reports based on a date and have a history of records. More on reporting in a future issue. Most ledgers are immutable, although it is not a strict requirement for double-entry accounting to function.

Payments on the ledger

Now our customer needs to pay the order. Our shop redirects the user to the payment flow, and the user completes the payment. The payment is a financial fact in our ledger. We receive $100 and need to reconcile this.

╭──────────────────────────────────────────────────┬───────┬────────╮
│ Ledger account                                   │ Debit │ Credit │
├──────────────────────────────────────────────────┼───────┼────────┤
│ balance_sheet.current_assets.accounts_receivable │   0.0 │  100.0 │
│ balance_sheet.current_assets.bank_account        │ 100.0 │    0.0 │
├──────────────────────────────────────────────────┼───────┼────────┤
│                                                  │ 100.0 │  100.0 │
╰──────────────────────────────────────────────────┴───────┴────────╯
Payment journal entries

We credit the account receivable ledger account. After this transaction, the balance for accounts receivable is $0, and we know we don't need to receive money from our customers. The bank account is increased by $100 because we received the money. The journal entries are balanced again.

You can see the benefit of double-entry bookkeeping when mistakes happen. For example, a customer's wire transfer has a lower amount of $99. Of course, you can book only $99 from accounts receivable, but sometimes you want to dismiss these minor errors because tracing them is too much work.

╭───────────────────────────────────────────────────┬───────┬────────╮
│ Ledger account                                    │ Debit │ Credit │
├───────────────────────────────────────────────────┼───────┼────────┤
│ balance_sheet.current_assets.accounts_receivable  │   0.0 │  100.0 │
│ balance_sheet.current_assets.bank_account         │  99.0 │    0.0 │
│ profit_loss.other_income_expenses.rounding_errors │   1.0 │    0.0 │
├───────────────────────────────────────────────────┼───────┼────────┤
│                                                   │ 100.0 │  100.0 │
╰───────────────────────────────────────────────────┴───────┴────────╯
Payment error journal entries

To be balanced, these journal entries need an extra $1 booking. You probably want to take this amount as business expenses like Rounding errors. Double-entry accounting forces us to decide how to handle the missing amount. Afterward, the $1 automatically reports on the correct ledger account, so the business can watch the amounts of rounding errors.

A more complex example

The above examples are still relatively easy. Imagine you have several different products with different tax rates. It is straightforward to make a mistake in the tax calculations, especially during rounding (trust me, I've been there!). Double-entry bookkeeping saves the day again because it detects these errors due to the imbalance of the entries. This error is caught during development or testing with strict database constraints, and you can fix your calculations.

╭──────────────────────────────────────────────────┬────────┬────────╮
│ Ledger account                                   │ Debit  │ Credit │
├──────────────────────────────────────────────────┼────────┼────────┤
│ balance_sheet.current_assets.accounts_receivable │ 211.75 │    0.0 │
│ balance_sheet.current_liabilities.taxes_payable  │    0.0 │  10.89 │
│ balance_sheet.current_liabilities.taxes_payable  │    0.0 │  13.86 │
│ profit_loss.revenue.consultancy                  │    0.0 │  121.0 │
│ profit_loss.revenue.recurring                    │    0.0 │   66.0 │
├──────────────────────────────────────────────────┼────────┼────────┤
│                                                  │ 211.75 │ 211.75 │
╰──────────────────────────────────────────────────┴────────┴────────╯
Journal entries for a sales invoice with multiple tax rates

A simple database table

In the following issues, I'll explain more about ledger accounts and sub-ledgers. Both concepts are needed to make double-entry accounting really powerful. I want to end this issue with the first version of the database schema for storing journal entries:

┌───────────────────┐
│   JournalEntry    │
├───────────────────┤
│ ledger_id         │
│ ledger_account_id │
│ date              │
│ debit             │
│ credit            │
└───────────────────┘
Database schema for journal entries

The table shows how powerful double-entry accounting is: a relatively simple data model can accommodate many scenarios. In upcoming issues, we will expand the table structure further.

Conclusion

There it is, the first introduction to double-entry accounting for software engineers. The examples are still relatively simple, but we'll get to the more complex situations. In the following issues, I'll explain more about the ledger account types, reporting, external ledgers, sub-ledgers, and reconciliation. Subscribe to the Balanced newsletter to receive the next issue directly in your mailbox!

📬
I'd love to hear from you! Let me know what you think or if you have questions. Do you already use double-entry accounting? Are you going to use it in the future? Please shoot me an e-mail at edwin@hey.com.