Double Entry Accounting DB Design

A Refined Database Schema for Double-Entry Accounting

Various blog posts have emerged introducing double-entry accounting to developers. In this article, a simple yet elegant database schema for recording and analyzing ledger entries in a way that aligns with conventional accounting practices.

Accounting Terminology in Development

Developers often sidestep accounting terminology, substituting terms like "debit" and "credit" with positive and negative numbers. While this seems simpler, it can lead to confusion. For example, the ledger-cli documentation notes:

"When you look at the balance totals for your ledger, you may be surprised to see that Expenses are a positive figure, and Income is a negative figure. It may take some getting used to, but…"

This confusion arises from representing credits as negative numbers, which doesn't match how financial statements are typically prepared. Let’s design a schema that avoids such inconsistencies and mirrors real-world accounting.


The Chart of Accounts

The first step is defining the list of accounts. This involves creating a table with the following columns:

  • Name: The account name (e.g., Assets, Liabilities).
  • Number: A hierarchical numeric identifier (e.g., 100 for Assets, 110 for Cash). This hierarchy allows easy roll-up of sub-account values.
  • Normal Balance: Indicates 1 for debit and -1 for credit. This column is for internal computations only.

Here’s the table structure in SQLite:

CREATE TABLE "accounts" (
  "name" TEXT,
  "number" INTEGER,
  "normal" INTEGER
);

Populate the table with some sample accounts:

Name Number Normal
Assets 100 1
Cash 110 1
Merchandise 120 1
Liabilities 200 -1
Deferred Revenue 210 -1
Revenues 300 -1
Expenses 400 1
Cost of Goods Sold 410 1
Equity 500 -1
Capital 510 -1

Deriving the Accounting Equation

Using SQL, we can derive the accounting equation:

SELECT
  group_concat(name, ' + ') AS expression
FROM accounts
GROUP BY normal;

Result:

  • Liabilities + Revenues + Equity + Deferred Revenue + Capital
  • Assets + Expenses + Cash + Merchandise + Cost of Goods Sold

To simplify, filter for high-level accounts:

SELECT
  group_concat(name, ' + ') AS expression
FROM accounts
WHERE number % 100 = 0
GROUP BY normal;

Result:

  • Liabilities + Revenues + Equity
  • Assets + Expenses

Transactions

With the chart of accounts in place, we can define a transactions table:

CREATE TABLE "transactions" (
  "id" INTEGER,
  "date" TEXT,
  "amount" REAL,
  "account" INTEGER,
  "direction" INTEGER
);

Columns:

  • ID: Groups related entries into a single transaction.
  • Date: Transaction date.
  • Amount: Dollar amount (always positive).
  • Account: References the account number.
  • Direction: Indicates debit (1) or credit (-1).

Example Transactions

Here’s a set of sample transactions for setting up a business, buying inventory, and making a sale:

ID Date Account DR CR Description
0 2022-01-01 Assets:Cash 500.0 Deposit capital into account
0 2022-01-01 Equity:Capital 500.0
1 2022-01-15 Assets:Merchandise 100.0 Purchase inventory
1 2022-01-15 Assets:Cash 100.0
2 2022-02-01 Assets:Cash 15.0 Customer prepayment
2 2022-02-01 Liabilities:Deferred Revenue 15.0

These transactions ensure that total debits equal total credits, maintaining the integrity of double-entry accounting.


Querying Transactions

Verifying Debits and Credits

Ensure debits equal credits with this query:

SELECT
  SUM(CASE WHEN direction = 1 THEN amount END) AS DR,
  SUM(CASE WHEN direction = -1 THEN amount END) AS CR
FROM transactions;

Result:

  • DR: 633.0
  • CR: 633.0

To identify mismatches:

SELECT
  id,
  SUM(direction * amount) AS balance
FROM transactions
GROUP BY id
HAVING balance != 0;

Calculating Balances

Generate a balance sheet with this query:

SELECT
  account,
  name,
  SUM(amount * direction * normal) AS balance
FROM transactions
LEFT JOIN accounts ON transactions.account = accounts.number
GROUP BY name
ORDER BY account, name;

Sample output:

Account Name Balance
110 Cash 415.0
120 Merchandise 97.0
300 Revenues 15.0
410 Cost of Goods Sold 3.0
510 Capital 500.0

To roll up sub-accounts into their main categories:

SELECT
  (account / 100) * 100 AS parent_account,
  name,
  SUM(amount * direction * normal) AS balance
FROM transactions
LEFT JOIN accounts ON transactions.account = accounts.number
GROUP BY parent_account
ORDER BY parent_account;

This schema offers a clear and efficient way to implement double-entry accounting in a database. With proper SQL queries, it supports detailed and aggregated financial analysis consistent with standard accounting practices.

Handy Commands

Find a process by port

lsof -i :[port number]

Close a stubborn process

kill -9 $(pgrep -f [process name] -u $USER)

Upgrade postgres in docker

sudo docker stop appname-postgres
sudo docker rm appname-postgres

sudo docker run -d --cpus=".8" \
-p 0.0.0.0:5432:5432 \
--restart unle...
            

David Heinemeier Hansson

Perhaps the best piece of advice I ever got from Jeff Bezos was this: Invest in things that don't change.


If you can keep your cool, and not thrash about reacting to every dip or peak, you usually make out pretty well in the end.

Joe Armstrong

Make it work, then make it beautiful, then if you really, really have to, make it fast. 90% of the time, if you make it beautiful, it will already be fast, so really, just make it BEAUTIFUL.

Derek Alexander Muller (Veritasium)

The 4 things it takes to be an expert: repeated attemps with feedback, valid environment, timely feedback, and don't get too comfortable.

Marty Cagan

Delivery teams are not cross-functional (basically just developers plus a backlog administrator product owner), they are not focused on outcome (they are all about output), and they are not empowered (they are there to code and ship).

Product teams are cross-functional, focused and measured by outcome, and empowered to come up with solutions that work.

Carl Gustav Jung

Where wisdom reigns, there is no conflict between thinking and feeling.

Jeff Sutherland

Estimating tasks will slow you down. Don’t do it. We gave it up over 10 years ago.

Today we have good data from Rally on 60,000 teams. The slowest estimate tasks in hours. No estimation at all will improve team performance over hour estimation.

Best teams have small stories and do no tasking. They move to acceptance test driven development.

Edsger Wybe Dijkstra

Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better.

IBM 1979

A computer can never be held accountable.

Therefore a computer must never make a management decision.

Larry Wall

There are three great virtues of a programmer; Laziness, Impatience and Hubris.