Designing an Event-Driven Ledger That Auditors Can Trust
Listen to this article
Preparing audio…
FinTech Engineering
I use events to distribute financial work, but I do not let the message stream declare itself the ledger. My starting point is an explicit posting boundary, correction model, and replay contract.

Three records that teams often confuse
Consider a wallet transfer of AED 250. The request to move funds is a command: it can be rejected for insufficient available balance. The balanced debit and credit are accounting facts: once booked, they are corrected with new entries rather than edited. TransferBooked is an integration event: a versioned notification that helps other services react.
These records can share an identifier, but they do not have the same authority. A Kafka topic is not automatically a journal, and an event-sourced workflow is not automatically double-entry accounting. If downstream consumers independently infer financial postings from an evolving business event, the same transfer can acquire different meanings in settlement, statements, and reporting.
Square’s engineering account of Books describes an immutable, balanced accounting service as the consistency primitive. AWS’s transactional outbox guidance explains the complementary delivery pattern: write domain state and an outbox record in one database transaction, then publish asynchronously. It also states that duplicates remain possible and consumers should be idempotent.
Put the transaction boundary around the posting
Here is how I would define the posting boundary. The posting service validates the journal before commit:
- every entry belongs to one transaction identifier;
- debits and credits balance for each currency or asset unit;
- accounts, dimensions, and posting rule version are valid;
- monetary values use explicit minor-unit or decimal precision;
- the command’s business key has not already produced a posting;
- recorded time, effective time, and source time are preserved separately.
Inside one database transaction, insert the immutable journal entries and an outbox row describing the committed transaction. Do not publish first and hope the database commit succeeds. Do not write the ledger and broker independently. The outbox closes that dual-write gap, but it does not provide exactly-once delivery; the published event must carry a stable event ID and transaction ID.
This model also clarifies what “event-driven ledger” should mean: events drive workflows around an authoritative ledger, and committed postings emit events. It does not mean any event stream becomes financially authoritative by declaration.
Design corrections as accounting events
Suppose an operator booked AED 250 to the wrong merchant account. Editing the row destroys the evidence seen by previous statements and consumers. Instead, post a reversal linked to the original transaction, then post the corrected transaction. The current balance changes, but the historical explanation remains intact.
Corrections need a reason code, initiator, approval evidence where required, original transaction reference, and a new idempotency key. “Reversal” should not be used as a generic delete. Some rails reverse a pending authorization; the ledger may instead record release of an encumbrance. The accounting representation must follow the product’s chart of accounts and posting policy.
Make time and versions first-class
Financial systems have several clocks. recorded_at tells auditors when the platform accepted the posting. effective_at tells statements and interest logic when its economic effect applies. source_occurred_at preserves the rail or provider timestamp. Late settlement data can legitimately be recorded today with an earlier effective date; collapsing the clocks makes period close and investigation ambiguous.
Event schemas evolve too. Include a schema version and stable semantic identifiers. Additive changes are easier to consume, but a changed posting meaning deserves a new event type or versioned contract. Keep the original payload when regulatory or operational evidence requires it, while ensuring secrets and personal data are protected.
Replay without paying twice
Projection consumers should store the last processed event or a deduplication record in the same transaction as their local update. A replay can rebuild balances, statements, and analytics from authoritative entries, but it must never call an external payment provider merely because an old event was read again.
Separate pure projections from effectful process managers. A statement projection can be rebuilt. A process manager that sends a payout needs its own durable command state and idempotency boundary. This distinction turns replay from a production hazard into a recovery tool.
Controls auditors can test
An audit-ready platform can demonstrate:
- the posting rule and code version that produced every journal;
- balance invariants checked at commit time;
- an unbroken link from command to posting to outbox event;
- corrections represented as linked entries;
- delivery lag, duplicate rate, dead-letter age, and projection checkpoints;
- periodic comparison of ledger totals with external settlement evidence.
Test the awkward moments: database commits but publisher crashes; broker redelivers after acknowledgement loss; a consumer deploy changes schema interpretation; a late correction crosses an accounting period; and a projection is rebuilt while live events continue. Recovery is complete only when balances and evidence agree.
The durable conclusion
My design rule is that events distribute a committed financial fact; they never manufacture its authority. I trust the ledger when balanced entries commit atomically, corrections preserve history, time semantics are explicit, and every derived event traces back to a posting.
Before production, document which database transaction contains the posting and outbox row, how the publisher checkpoints, and how each consumer deduplicates. Rebuild a projection in a clean environment and compare it with production totals. Then simulate a publisher crash, schema rollback, and late correction across a period boundary. If the team cannot explain the resulting journal and consumer checkpoints, replay is not yet a safe recovery mechanism.
What decision would you make?
Add a question, a field note, or a respectful counterpoint. Comments are for signed-in members so the discussion stays useful and professional.