Ledger double-entry: how we designed an accounting engine that never loses a centavo
Every transaction in Revenu generates automatic double-entry postings. Here's how our 11-engine ledger handles multi-currency, COSIF compliance, real-time reconciliation, and why most fintech ledgers are ticking time bombs. The architecture behind a ledger that has processed R$ 8 billion with zero balance discrepancies.

Why double-entry matters in fintech — and why most fintechs get it wrong
Single-entry systems track balances. Double-entry systems track the movement of value. For regulated financial institutions, this isn't optional — it's required.
But here's what most fintech engineers don't understand: double-entry bookkeeping isn't just a regulatory checkbox. It's the only accounting system that is self-proving. Every debit has a corresponding credit. If total debits don't equal total credits, something is wrong — and you can find it.
Single-entry systems (which is what most fintechs actually build, even if they call it a "ledger") can't tell you when something is wrong. They track a balance: +R$ 100, -R$ 50, balance = R$ 50. If a bug adds R$ 100 without subtracting it from somewhere, the balance says R$ 150 and nobody knows there's a problem until a reconciliation report catches it — days or weeks later.
In a double-entry system, that bug is impossible. You can't credit an account without debiting another. The system enforces conservation of value at the transaction level, not as an after-the-fact check.
The fintech ledger epidemic: balance tables pretending to be ledgers
Let me describe what 80% of fintechs call their "ledger":
A PostgreSQL table called balances with columns: account_id, currency, available, pending, blocked. When a payment comes in, the system runs UPDATE balances SET available = available + 100 WHERE account_id = 'abc'.
This isn't a ledger. It's a balance tracker. And it has fatal flaws:
No audit trail
UPDATE overwrites the previous value. Once the balance changes from R$ 500 to R$ 600, there's no record of the R$ 500 state unless you separately log it. Most don't.
Race conditions
Two concurrent transactions read available = 500, both add R$ 100, both write R$ 600. The account should have R$ 700 but has R$ 600. You just created R$ 100 out of thin air — or destroyed it, depending on the operation.
Yes, SELECT FOR UPDATE and row-level locks help. But they serialize all operations on a single account, creating a performance bottleneck that limits throughput to ~1,000 TPS per account.
No reconciliation capability
How do you verify the balance is correct? You'd need to replay all historical transactions. But you don't have them — you only have the current balance. You can't prove the balance is right because you threw away the evidence.
Rounding errors compound silently
When you compute a 12.5% platform fee on R$ 99.99 (= R$ 12.49875), you round to R$ 12.50. Over millions of transactions, these rounding errors accumulate. In a single-entry system, nobody notices until end-of-quarter reconciliation reveals a R$ 47,000 discrepancy. Good luck explaining that to your auditor.
Revenu's ledger architecture: the 11 engines
Our ledger isn't a single table or even a single service. It's a system of 11 specialized engines, each responsible for a specific domain of financial accounting:
1. Core Ledger Engine
The foundation. Handles double-entry postings, enforces the accounting equation (Assets = Liabilities + Equity), and guarantees that every transaction balances to zero.
Every posting is a pair: DEBIT account_a, CREDIT account_b, amount, currency. Complex operations (like splits) create multiple posting pairs — all within a single atomic transaction.
2. Chart of Accounts Engine
Manages the hierarchical account structure. Every account in Revenu is classified according to a chart of accounts that maps to COSIF (Plano Contábil das Instituições do Sistema Financeiro Nacional) — BACEN's mandatory accounting classification.
Account hierarchy example:
- 1.0.0.00.00-0 ASSETS
- 1.1.0.00.00-0 Cash and equivalents
- 1.1.1.00.00-0 Bank reserves
- 1.3.0.00.00-0 Interbank operations
- 1.3.1.00.00-0 PIX clearing
- 1.3.2.00.00-0 TED clearing
This mapping means every internal ledger entry automatically classifies into the correct COSIF account. CADOC reporting becomes a projection of existing data, not a manual mapping exercise.
3. Multi-Currency Engine
Handles accounts denominated in different currencies, exchange rate tracking, and currency conversion postings.
When a cross-border payment converts USD to BRL, the engine creates:
DEBIT: client_usd_account USD 1,000.00
CREDIT: fx_clearing_usd USD 1,000.00
DEBIT: fx_clearing_brl BRL 5,120.00
CREDIT: client_brl_account BRL 5,120.00
DEBIT: fx_clearing_usd USD 1,000.00
CREDIT: fx_clearing_brl BRL 5,120.00 (contra-entry)
The FX spread (if any) is captured as a separate posting to the platform's FX revenue account. The ledger tracks the exact exchange rate used, the conversion timestamp, and the rate source — all immutably.
4. Escrow Engine
Manages escrow accounts for marketplace splits, payment holds, and settlement schedules. Each escrow entry is a separate ledger account with its own lifecycle (PENDING → AVAILABLE → SETTLED or REVERSED).
5. Fee Engine
Calculates and posts platform fees, MDR, interchange, gateway costs, and anticipation fees. Every fee is a separate posting pair — never a silent deduction from a balance.
When a seller receives R$ 1,000 with a 3% MDR:
DEBIT: buyer_clearing R$ 1,000
CREDIT: seller_escrow R$ 970
CREDIT: platform_mdr_revenue R$ 30
The seller sees R$ 970. The platform sees R$ 30 in fee revenue. The clearing account balances to zero. Three postings, one atomic transaction, perfect audit trail.
6. Provisioning Engine
Handles provisions for expected losses — credit loss provisions, chargeback provisions, and regulatory capital requirements. Provisions are reversed when the expected loss doesn't materialize or adjusted when actual losses differ from projections.
7. Accrual Engine
Manages time-based value recognition: interest accrual on loans, fee recognition over service periods, and deferred revenue. The engine runs daily accrual jobs that create postings automatically based on configured rules.
8. Tax Engine
Calculates and posts IOF (financial operations tax), IRRF (withholding income tax), ISS (municipal service tax), and PIS/COFINS contributions. Tax postings are separate from the underlying transaction — if tax rules change, historical postings remain untouched and new rules apply going forward.
9. Reconciliation Engine
Continuously reconciles internal ledger state against external sources: bank statements (CNAB 240 / camt.053), PSTI settlement reports, card network settlement files, and boleto clearing reports.
The engine runs reconciliation as a streaming process, not a batch job. Each external event is matched against internal postings in real-time. Unmatched items are flagged immediately.
10. Reporting Engine
Generates COSIF-compliant financial statements, balance sheets, income statements, and cash flow reports — all from ledger data. Because every posting maps to a COSIF account, reports are projections of existing data, not separate calculations.
11. Archive Engine
Handles ledger data lifecycle: compression of historical postings, offloading to cold storage, and guaranteed retrieval for audit requests. Regulatory requirement: 10 years of complete ledger history, accessible within 48 hours.
The immutability principle: why we never UPDATE
Every ledger entry in Revenu is immutable. Once written, it cannot be modified or deleted. Ever.
This isn't just good practice — it's an architectural invariant enforced at every layer:
Database level
The ledger tables have no UPDATE or DELETE permissions. The application database user can only INSERT and SELECT. Even DBAs with superuser access would trigger alerts if they attempted an UPDATE on ledger tables.
Application level
The ledger service exposes only two operations: post(entries) and query(filters). There is no update or delete endpoint. The API literally cannot modify existing entries.
Correction mechanism
What happens when something is wrong? You don't fix the wrong entry — you create a new reversal entry that neutralizes it, then create a new corrected entry.
Original (wrong): DEBIT: account_a R$ 100, CREDIT: account_b R$ 100
Reversal: DEBIT: account_b R$ 100, CREDIT: account_a R$ 100
Corrected: DEBIT: account_a R$ 110, CREDIT: account_b R$ 110
The audit trail now shows: the original entry, that it was reversed (with reason code and timestamp), and the corrected entry. An auditor can see exactly what happened, when, and why. Nothing is hidden.
Event Sourcing + double-entry: a natural fit
Our ledger is built on Event Sourcing. Every financial operation in Revenu emits domain events. The ledger is a projection of those events.
PaymentReceived { amount: 1000, from: buyer, to: clearing }
→ DEBIT: buyer_account R$ 1,000 / CREDIT: clearing_account R$ 1,000
SplitExecuted { recipients: [{ seller_a: 400 }, { seller_b: 350 }, { seller_c: 250 }], fee: 120 }
→ Multiple posting pairs, one per recipient + fee
SettlementCompleted { seller: seller_a, amount: 400, method: PIX }
→ DEBIT: escrow_seller_a R$ 400 / CREDIT: settlement_clearing R$ 400
Because events are immutable and ordered, the ledger can be rebuilt from scratch by replaying all events. We do this periodically as a verification: rebuild the entire ledger from the event store and compare it to the live ledger. If they match (they always have), we know the ledger is correct.
This is the ultimate audit guarantee: the ledger isn't just auditable — it's reconstructable.
Real-time reconciliation: catching discrepancies in milliseconds
Traditional reconciliation is a batch process. At end-of-day, someone (or some job) compares internal records against external bank statements. Discrepancies found today might relate to transactions from 3 days ago. Good luck investigating a 3-day-old discrepancy when you process 100,000 transactions per day.
Our reconciliation engine operates in real-time:
Event-driven matching
When an external event arrives (PIX confirmation from SPI, TED confirmation from CIP, boleto payment from clearing house), the reconciliation engine immediately matches it against the corresponding internal posting.
Match found? Status updated to RECONCILED. No match? Alert fires within seconds.
Continuous balance verification
Every 60 seconds, the engine verifies the fundamental accounting equation across all accounts:
SUM(all_debits) == SUM(all_credits)
If this equation doesn't hold — even by R$ 0.01 — an alert fires immediately. We catch discrepancies in the minute they occur, not days later.
Cross-system reconciliation
The engine reconciles across multiple external systems simultaneously:
- PIX settlements via SPI (pacs.002 confirmations)
- TED settlements via CIP
- Boleto payments via clearing house files
- Card network settlements via Visa/Mastercard files
- Bank account balances via camt.053 statements
Each external system is a reconciliation stream. All streams are processed in real-time. The reconciliation dashboard shows a live view of matched vs. unmatched items across all systems.
COSIF compliance: accounting that regulators understand
COSIF (Plano Contábil das Instituições do Sistema Financeiro Nacional) is BACEN's mandatory chart of accounts for all financial institutions in Brazil. Every balance, every transaction, every report must map to COSIF account codes.
Most fintechs handle COSIF as a translation layer — their internal accounts use arbitrary codes, and a separate process maps them to COSIF for reporting. This creates the same translation-layer problems we discussed in the ISO 20022 post: data loss, mapping errors, and maintenance burden.
Revenu's chart of accounts is natively COSIF-aligned. Every internal account maps 1:1 to a COSIF code. When we need to generate a COSIF-compliant balance sheet, we query the ledger directly — no translation, no mapping, no approximation.
This means:
- CADOC documents are generated from live ledger data
- Regulatory balance sheets are always consistent with operational data
- Audit requests are answered by querying the ledger, not by reconstructing from disparate systems
Performance: how we handle 50,000 postings per second
A naive double-entry ledger serializes all operations through a single lock. This limits throughput to ~1,000 TPS. For a payment processor handling PIX (which settles in < 2 seconds), this isn't enough.
Account-level parallelism
Transactions that touch different accounts can execute in parallel. Only transactions touching the same account are serialized. Since our account space is large (millions of accounts), parallelism is high.
Batch posting
Multiple posting pairs from the same business operation are batched into a single database transaction. A split payment with 10 recipients generates 10 posting pairs — all written in one atomic batch, not 10 sequential inserts.
Append-only writes
Immutable ledger entries are append-only. No row locks, no read-modify-write cycles. Just INSERT. This is the fastest write pattern for any database.
Read replicas for queries
Balance queries and reports read from dedicated replicas, never from the write path. This eliminates read/write contention. Balance views are eventually consistent with a lag of < 100ms.
The numbers
- 50,000 postings/second sustained throughput
- < 5ms per posting pair (p99)
- < 100ms balance query response time
- Zero write conflicts — append-only architecture eliminates lock contention
The numbers from production
After 18 months and R$ 8 billion in processed volume:
- Balance discrepancy: R$ 0.00 — the ledger has never been out of balance
- 11 specialized engines working in concert
- 50,000 postings/second sustained throughput
- < 5ms posting latency (p99)
- 100% COSIF alignment — no translation layers for regulatory reporting
- Real-time reconciliation — discrepancies caught in < 60 seconds
- 0 manual corrections — all corrections are automated reversal + re-posting
- 10-year archive — complete ledger history, retrievable in < 48 hours
- Ledger rebuild verification: monthly full rebuild from event store — 100% match every time
Why this matters
A ledger isn't a feature. It's the foundation of everything else: payments, compliance, reporting, reconciliation, audit. If the ledger is wrong, everything built on top of it is wrong.
Most fintechs discover their ledger is wrong when they can't close their books at month-end. Or when an auditor finds a discrepancy they can't explain. Or when a regulatory report doesn't match the operational data. By then, the cost of fixing it is enormous — sometimes existential.
We designed our ledger to make these scenarios impossible. Not unlikely — impossible. Conservation of value is enforced at the transaction level. Immutability is enforced at the database level. Reconciliation runs continuously. The accounting equation is verified every 60 seconds.
The result: a ledger you can trust. Not because someone says it's correct, but because the architecture makes it mathematically impossible for it to be wrong.
That's what double-entry accounting was designed for 700 years ago. We just implemented it properly.