Browse Templates/Accounting System

Accounting System

A professional double-entry accounting database schema following GAAP/IFRS principles. Features a hierarchical chart of accounts (assets, liabilities, equity, revenue, expenses), journal entries with balanced debit/credit lines, invoice generation with line items, and full audit trail. Suitable for building accounting software like QuickBooks or Xero, ERP financial modules, or bookkeeping platforms.

PostgreSQL10 tablesFinance
financeaccountingdouble-entryinvoicesjournal-entriesbookkeeping
Created with ERD Studio

About this schema

The rule that governs this schema is not written in any column: for every journal_entries row, the sum of its journal_lines debits must equal the sum of its credits. Nothing in the table definitions enforces it, which is why the entry carries a status — a draft can be unbalanced, a posted entry cannot.

Debits and credits are separate columns rather than one signed amount. That mirrors how ledgers are actually read and kept, and it makes an unbalanced entry visible as two different sums instead of a total that happens not to be zero. Each line fills exactly one of the two.

Invoices do not replace journal entries; they produce them. invoices.journal_entry_id links the commercial document to its accounting effect, so the invoice can be revised, voided or chased while the ledger keeps the immutable record of what was posted and when.

Key relationships

account_categories → accounts → accounts (parent_id)
The chart of accounts is a tree under a category whose normal_balance says whether a debit increases or decreases it.
journal_entries → journal_lines → accounts
The entry is the transaction, the lines are its debits and credits, and each line names the account it moves.
journal_entries → fiscal_periods
Every entry belongs to a period, and is_closed on the period is what stops a posting changing a month that has already been reported.
invoices → journal_entries
The document and its ledger effect are separate rows, so voiding an invoice is a reversing entry rather than an edit to history.
invoice_items → tax_rates
The rate is referenced per line, so an invoice can mix taxable and exempt items and still recompute correctly.
customers / vendors
Kept apart because receivables and payables are different sides of the ledger, and a vendor carries payment_terms a customer does not.

Design decisions

Debit and credit are two columns

A single signed amount would be smaller and would let a line be both at once by accident. Two columns make the convention explicit, let the balance check be two sums compared, and match every report an accountant already knows how to read. The cost is a constraint the schema cannot express — exactly one of the pair must be non-zero — which has to live in the application or a check constraint.

Nothing is ever edited, only reversed

status on journal_entries exists so a posted entry becomes immutable. Correcting a mistake means posting a reversing entry and a new correct one, which leaves three rows telling the truth about what happened. Editing in place would be simpler and would make the ledger unauditable — the entire point of double-entry is that history cannot be quietly rewritten.

fiscal_periods carries is_closed

Once a period is closed and reported, a new posting into it changes numbers that have already been published. The flag makes that a rejected write rather than a discovered discrepancy months later. It also gives the close process somewhere to record itself, with closed_at as the moment the books stopped moving.

normal_balance lives on the category, not the account

Assets and expenses increase with a debit; liabilities, equity and income increase with a credit. That is a property of the five classifications, not of individual accounts, so storing it per category keeps one definition and makes every account inherit the right sign. Putting it on each account would invite a cash account configured as a liability.

Tables in this template

accountsChart of Accounts
8 cols
account_categoriesAccount Categories
4 cols
journal_entriesJournal Entries
9 cols
journal_linesJournal Lines
6 cols
customersCustomers
7 cols
vendorsVendors
7 cols
invoicesInvoices
9 cols
invoice_itemsInvoice Items
7 cols
tax_ratesTax Rates
5 cols
fiscal_periodsFiscal Periods
7 cols

Frequently asked

What stops an unbalanced journal entry?
Nothing in the schema — it is enforced when posting. The debit and credit sums are compared before status moves to posted, and total_amount on the entry records the agreed figure so a later mismatch is detectable.
Why does an invoice link to a journal entry?
Because the invoice is a document and the entry is its accounting effect. Issuing an invoice debits receivables and credits income; the link keeps the two connected while letting each have its own lifecycle.
How do I correct a posted entry?
Post a reversing entry that mirrors the original's debits and credits, then post the corrected one. The original stays, which is what makes the ledger auditable — the correction is part of the history, not a replacement for it.
Why is the chart of accounts a tree?
So accounts roll up into subtotals — individual bank accounts under cash, cash under current assets. parent_id lets reporting aggregate at any level without a fixed depth or a separate hierarchy table.
Where are payments against an invoice recorded?
As journal entries, not as a payments table. Receiving money debits cash and credits receivables, and the invoice's status reflects it — which is the double-entry answer, though most systems add a payments table for reconciliation convenience.