5 Maret 20268 min read

Engineering Reliable Double-Entry Accounting Systems in Modern Web Apps

Bridging software engineering and accounting principles: immutable ledgers, ACID transaction guarantees, debit/credit balancing, and audit trails.

AccountingDatabaseFintechArchitecturePostgreSQL

As an Accounting Information Systems student and Fullstack Developer, I frequently see financial applications rely on simplistic balance = balance + amount mutations. In financial software, this is dangerous: balance is a derived calculation, not a mutable state.


The Golden Rule of Double-Entry Bookkeeping

Every financial transaction consists of at least two journal entries: $$\sum \text{Debits} = \sum \text{Credits}$$

An immutable ledger guarantees that money is never created or destroyed out of thin air.


Schema Architecture in PostgreSQL

CREATE TABLE accounts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    account_number VARCHAR(32) UNIQUE NOT NULL,
    name VARCHAR(128) NOT NULL,
    type VARCHAR(32) NOT NULL CHECK (type IN ('ASSET', 'LIABILITY', 'EQUITY', 'REVENUE', 'EXPENSE')),
    currency VARCHAR(3) DEFAULT 'IDR'
);

CREATE TABLE transactions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    reference_id VARCHAR(64) UNIQUE,
    description TEXT NOT NULL,
    posted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE journal_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    transaction_id UUID REFERENCES transactions(id) ON DELETE RESTRICT,
    account_id UUID REFERENCES accounts(id),
    amount NUMERIC(18, 4) NOT NULL,
    direction VARCHAR(6) NOT NULL CHECK (direction IN ('DEBIT', 'CREDIT')),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Enforcing Transaction Balance with Database Triggers

CREATE OR REPLACE FUNCTION verify_transaction_balance() 
RETURNS TRIGGER AS $$
DECLARE
    total_debit NUMERIC(18, 4);
    total_credit NUMERIC(18, 4);
BEGIN
    SELECT 
        COALESCE(SUM(CASE WHEN direction = 'DEBIT' THEN amount ELSE 0 END), 0),
        COALESCE(SUM(CASE WHEN direction = 'CREDIT' THEN amount ELSE 0 END), 0)
    INTO total_debit, total_credit
    FROM journal_entries
    WHERE transaction_id = NEW.transaction_id;

    IF total_debit <> total_credit THEN
        RAISE EXCEPTION 'Transaction unbalanced! Total Debits: %, Total Credits: %', total_debit, total_credit;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

This architecture guarantees mathematical precision, zero balance corruption, and 100% compliance with audit standards.

Bagikan

Artikel lainnya