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.