Event Sourcing in DoubleEntryLedger
View SourceOverview
Commands are the write-ahead log of DoubleEntryLedger. Each call to DoubleEntryLedger.Apis.CommandApi creates an immutable Command record containing:
- The original request payload (
command_map) - Idempotency keys (
source,source_idempk, optionalupdate_idempk) - A
CommandQueueItemthat tracks processing attempts and outcomes
Commands are never updated; only the queue item changes as work progresses. When a worker finishes successfully it emits a JournalEvent plus the necessary projections (transactions, entries, balance history, updated accounts) and links them together for auditing. JournalEvents are immutable and act as the event source for the ledger. Commands on the other hand could potentially be removed once they are processed.
Processing and replay
- Statuses:
CommandQueueItemdrives the lifecycle (:pending,:processing,:processed,:failed,:occ_timeout,:dead_letter). Each transition records timestamps, processor IDs, retry counts, and error payloads. - Replay order: For most read scenarios it is easiest to replay
JournalEventrecords (ordered byinserted_at) because they capture the canonical “business fact” for both account and transaction commands. - Idempotency:
DoubleEntryLedger.Command.IdempotencyKeyhashes(instance_id, source, source_idempk, update_idempk)to guarantee that duplicate requests are not processed again.PendingTransactionLookupties updates to the transaction created by the original pending command.
Account balance projections
- Account state: The
Accountschema stores embeddedBalancestructs forpostedandpendingvalues plus anavailableinteger that reflects the correct sign based on the account’s normal balance. - Balance history:
BalanceHistoryEntryrows are appended for every entry mutation so you can audit how each command changed an account’s posted or pending amounts. Each history row links to the originatingEntry, which links back to theTransaction,JournalEvent, andCommand. - Consistency checks:
InstanceStore.validate_account_balances/1recalculates sums across accounts, ensuring debits and credits match per currency for both posted and pending projections.
Benefits
- Auditability: Immutable commands, journal events, and balance history entries make it trivial to trace any change back to the originating API call.
- Rebuildability: Replay journal events to reconstruct accounts, transactions, and balances if you need to recover from a bug or rebuild analytics projections.
- Resilience: Command queue retries isolate transient failures while keeping the authoritative log append-only.
- Transparency:
JournalEventTransactionLinkandJournalEventAccountLinktables capture every relationship so you can answer “which command touched this transaction/account?” instantly.
Immutability considerations
Application logic enforces immutability today: commands and journal events are only appended, balance history is append-only, and updates to Account or Transaction rows can only happen through the command workers. PostgreSQL row-level security or restricted grants can make this enforcement stricter if your deployment shares a database with other applications.