Here's a number that should make you uncomfortable: 78% of American workers live paycheck to paycheck. Not just minimum wage workers. Households earning over $100,000 a year are in that group too. Inflation has pushed grocery bills up 25% since 2020. Rent in major cities has climbed 30-40%. Interest rates have made borrowing more expensive than it's been in two decades.
The natural response is to track your spending. And most people start the same way: a spreadsheet. One column for the date, one for the amount, one for the category. Maybe a few formulas at the bottom.
It works for a month. Maybe two. Then things get messy:
These aren't edge cases. They're the normal reality of personal finance. And the accounting world solved every one of these problems 500 years ago.
In 1494, a Franciscan friar named Luca Pacioli published Summa de Arithmetica in Venice. It was a mathematics textbook, but buried inside was a 27-page section called "Particularis de Computis et Scripturis" (details of calculation and recording). It described the bookkeeping system that Venetian merchants had been using for over a century.
The system was dead simple: every financial event must be recorded in two places.
Buy a shipment of spices for 100 ducats? Your Merchandise account goes up by 100, and your Cash account goes down by 100. The total across all your accounts? Still zero. Always zero. If it's not zero, someone made an error, and you can find it.
This wasn't just clever record-keeping. It was a self-auditing system. Before double-entry, merchants could only tell if they were profitable by counting their money and comparing it to what they started with. With double-entry, they could see where the money went, when it moved, and whether every transaction was accounted for.
Double-entry bookkeeping rests on three ideas:
When you spend $50 on groceries with your debit card:
When you receive a $3,000 paycheck:
Nothing appears from nowhere. Nothing vanishes. Money always moves from somewhere to somewhere.
Every transaction is recorded as a set of debits and credits. The total debits must always equal the total credits. No exceptions.
This is confusing at first because "debit" and "credit" don't mean what you think they mean. Forget your debit card and credit card. Those are marketing terms. In accounting:
| Account Type | Debit (increases) | Credit (increases) |
|---|---|---|
| Asset (what you own) | Debit | |
| Expense (what you spend) | Debit | |
| Liability (what you owe) | Credit | |
| Income (what you earn) | Credit | |
| Equity (net worth) | Credit |
So when you buy $50 of groceries with your debit card:
Total debits: $50. Total credits: $50. Balanced.
Assets = Liabilities + Equity + (Income - Expenses)
This equation is the ultimate check. At any point in time, if you sum up all your assets, they must equal the sum of your liabilities, equity, and net income. If they don't, there's an error in your books.
This isn't just theory. We're going to enforce this in SQL with constraints and triggers.
You could implement double-entry in any language. But SQL is the natural fit because the guarantees of double-entry map directly to database features:
| Accounting Requirement | SQL Feature |
|---|---|
| Every transaction must balance | CHECK constraint on sum of debits and credits |
| Accounts must reference valid types | FOREIGN KEY constraints |
| Posted transactions can't be modified | TRIGGER that blocks updates |
| Every change must be logged | Audit TRIGGER writing to a log table |
| Reports aggregate across accounts | GROUP BY, SUM, window functions |
| Account hierarchies roll up | Self-referencing FKs, recursive CTEs |
The database enforces correctness. Not your application code, not your API layer, the database itself. Even if you bypass the UI entirely and run raw SQL, the constraints won't let you create an unbalanced transaction.
That's a level of data integrity that spreadsheets and most apps can't match.
Over the next few articles, we'll build a complete budget tracking system from the ground up:
Each article builds on the previous one. The schema is cumulative. By the end, you'll have a working system with real accounting rigor behind it.
Here's a high-level view of where we're headed:
account_types accounts journal_entries
├── id ├── id ├── id
├── name ├── name ├── date
├── normal_balance ├── account_type_id ──┤ ├── description
└── description ├── parent_account_id │ ├── is_posted
├── account_code │ └── created_at
└── is_active │
│
journal_lines ────────┘ audit_log
├── id ├── id
├── journal_entry_id ├── table_name
├── account_id ├── action
├── debit ├── old_data (JSONB)
├── credit ├── new_data (JSONB)
└── description └── changed_at
Four core tables, one audit table. The audit_log is a generic change-tracking table. The table_name column identifies which entity changed, and old_data/new_data store the before/after state as JSONB. We'll attach triggers selectively to the tables that matter most: journal_lines (prevent tampering with posted transactions), journal_entries (track status changes), and accounts (track deactivations and renames). Not every table needs auditing, just the ones where unauthorized or accidental changes would cause real problems.
Let's start building.
SQL Output