Skip to content
Data Modelstable

Money and ledger data model

Money as integer minor units, a double-entry ledger whose lines always sum to zero, and the constraints that make “the books don’t balance” a write error instead of a month-end discovery.

This spec defines how monetary value is stored, computed, and reconciled. It exists because a floating-point cent and a nullable price are silent liabilities: they don't error, they drift. The model stores money as exact integers, records movement as balanced double-entry lines, and pushes the balancing invariant down to a database constraint.

Definition

Representation

Every monetary amount is a signed integer count of the currency's minor unit (cents) plus an explicit currency code. There is no floating-point money anywhere in the system — not in a column, not in an intermediate calculation, not in an API payload. Display formatting is a presentation concern applied once, at the edge.

ColumnTypeConstraintNotes
amount_minorBIGINTNOT NULLSigned integer count of minor units (cents)
currencyCHAR(3)NOT NULL, ISO 4217No mixed-currency arithmetic without conversion
— (never)FLOAT/DOUBLEprohibitedBinary floats cannot represent 0.10 exactly
Money column convention

Entity model

Entity-relationship model

A double-entry ledger: an account holds a balance, a transaction is an atomic financial event, and every transaction is composed of two or more lines that reference accounts. The sum of a transaction's line amounts is always zero — value moves between accounts, it is never created or destroyed on a line.

Ledger ERD (paste into any Mermaid renderer)
Ledger ERD (paste into any Mermaid renderer)

Definition

The balancing constraint

The core invariant — every transaction's lines sum to zero — is enforced by the database, not by application discipline. A transaction that does not balance cannot commit. This turns a whole class of reconciliation bugs into an immediate, un-ignorable write failure.

Constraints that make imbalance impossible to persist
-- Amounts are exact integers, and non-negative where the domain requires it.
ALTER TABLE products
  ADD CONSTRAINT price_non_negative CHECK (price_minor >= 0);

-- A deferred constraint trigger asserts each transaction nets to zero
-- at COMMIT, after all its lines are inserted.
CREATE CONSTRAINT TRIGGER transaction_balances
  AFTER INSERT OR UPDATE ON ledger_lines
  DEFERRABLE INITIALLY DEFERRED
  FOR EACH ROW EXECUTE FUNCTION assert_transaction_balances();
-- assert_transaction_balances():
--   SELECT SUM(amount_minor) FROM ledger_lines WHERE transaction_id = NEW.transaction_id;
--   IF <> 0 THEN RAISE EXCEPTION 'transaction % does not balance', NEW.transaction_id;

Definition

Rounding and allocation

When a value must be split — tax, discount, revenue share — the remainder is distributed deterministically so the parts always re-sum to the whole. No cent is lost to independent rounding. Allocation is a defined operation, not an ad-hoc `round()` per part.

Largest-remainder allocation — parts always re-sum to the whole
def allocate(total_minor: int, weights: list[int]) -> list[int]:
    """Split total_minor across weights so the result sums back to total_minor."""
    w = sum(weights)
    base = [total_minor * x // w for x in weights]
    remainder = total_minor - sum(base)          # the cents rounding dropped
    # Hand the leftover cents to the largest fractional parts, deterministically.
    order = sorted(range(len(weights)),
                   key=lambda i: (total_minor * weights[i]) % w, reverse=True)
    for i in order[:remainder]:
        base[i] += 1
    assert sum(base) == total_minor              # invariant, always holds
    return base

Invariants this spec guarantees

  • No monetary value is ever represented or computed as a floating-point number.
  • Every transaction's ledger lines sum to exactly zero; an unbalanced transaction cannot commit.
  • A split value's parts always re-sum to the original; rounding never creates or destroys a cent.
  • Prices and balances cannot be persisted negative where the domain forbids it.

Want this specified for your system?

We turn definitions like these into the actual schema, policies, and contracts your system runs on. Fixed scope, fixed price, defined delivery date.

Request a Fixed-Scope Architecture Blueprint