All posts
· 5 min read · Siva Sethu Saravanan S
A ledger is not a table of payments
The double-entry model behind PixelPay, explained with the three bugs it made impossible.
The first version of most payment dashboards is a payments table with a status column.
It works until the first refund, the first partial payout or the first currency
conversion. Then the numbers stop adding up and nobody can say why.
PixelPay uses double-entry bookkeeping instead. Every movement of money is two entries that sum to zero.
The model
create table entries (
id bigserial primary key,
transfer_id uuid not null,
account_id uuid not null,
amount bigint not null, -- minor units, signed
currency char(3) not null,
created_at timestamptz not null default now()
);
A balance is never stored. It is the sum of an account's entries.
Three bugs this made impossible
- Money appearing from nowhere. A transfer whose entries do not sum to zero is rejected by a constraint, not by code review.
- Silent edits. Entries are never updated. A correction is a new transfer that reverses the old one, so history stays intact.
- Mismatched payouts. Fees are their own account. The gap between what a customer paid and what landed in the bank is always explained.
The trade-off
Reads need aggregation, so the dashboard is served from pre-computed views that are refreshed as entries arrive. That is extra machinery, but it is machinery whose answers you can prove.