journaling.md 6.3 KB

Journaling

Kuatia supports journaling. A committed transfer is a journal entry, and the transfer log is the accounting journal: the append-only, ordered book of every committed entry. This page states what that means in practice, shows single, compound, and multi-asset entries, and explains why the log is auditable by replay.

For the vocabulary mapping (journal vs. journal entry vs. Book, and the terms that are easy to conflate) see accounting-mapping.md. For the resolve algorithm and the builder API see transfers.md. For the decision record see ADR-0013.

What "journaling" means here

In classical bookkeeping:

  • A journal is the book of original entry: a chronological, append-only record of every accounting event.
  • A journal entry is one balanced event. Its debits equal its credits.
  • A compound journal entry is one event that touches more than two accounts.

Kuatia provides the same guarantees over a UTXO-style, posting-based model:

Classical accounting Kuatia
Journal (book of original entry) Transfer log (TransferStore of EnvelopeRecords)
Journal entry (one balanced event) Committed Transfer resolved into an Envelope
Compound journal entry Transfer with multiple Movements
Σ debits = Σ credits Per-asset conservation sum(consumed) == sum(created)

The mechanism differs (signed postings instead of mutable balances), the accounting grain is identical: one committed transfer is one journal entry.

A journal entry: one balanced event

A Transfer is a Vec<Movement> committed atomically. Each Movement is { from, to, asset, amount }. On commit, resolve() turns the movements into an Envelope of concrete postings to consume and create, and validation enforces per-asset conservation before anything is written. The whole entry commits or none of it does.

A minimal two-account entry:

let transfer = TransferBuilder::new()
    .pay(alice, bob, usd, Cent::from(50))
    .build();

This is one journal entry: 50 USD leaves Alice, 50 USD arrives at Bob, and the resolved envelope balances (sum(consumed) == sum(created) for USD).

Compound entries: more than two accounts

A compound journal entry is native, not a special case. Because a Transfer holds a list of movements, chaining builder calls accumulates legs into one atomic entry.

Classical compound entry (a cash sale with tax):

2026-06-26  Cash sale of goods
  Dr  Cash ................. 115
      Cr  Revenue ..........     100
      Cr  Sales tax payable .      15

The equivalent business effects as one Kuatia transfer:

let transfer = TransferBuilder::new()
    .book(sales_book)
    .pay(customer, revenue, usd, Cent::from(100))
    .pay(customer, tax_payable, usd, Cent::from(15))
    .build();
// One Transfer -> one Envelope -> one EnvelopeRecord in the transfer log.

Both are a single balanced event. In the classical entry Σ Dr (115) = Σ Cr (115). In Kuatia the resolved envelope satisfies per-asset conservation for USD. The resolve step aggregates net debit per (account, asset) before selecting postings, so several legs debiting the same account share one selection pass. See transfers.md for the aggregation detail, and the note in accounting-mapping.md on why this shows business effects rather than a literal debit/credit translation.

Multi-asset entries

Conservation is enforced per asset, and each asset is an independent conservation boundary. A single entry can therefore move more than one asset as long as every asset balances on its own. This is how a currency exchange or FX trade is recorded as one journal entry: the USD legs balance against USD, and the EUR legs balance against EUR, within the same envelope.

A hand-built multi-asset envelope is committed through the same path with ledger.commit_envelope(envelope). Validation returns ConservationViolation { asset, consumed_sum, created_sum } if any single asset fails to balance.

The transfer log is the journal

One entry and the journal differ only in grain:

  • A Transfer / Envelope is one record: one journal entry.
  • The transfer log is the collection of all records: the accounting journal. TransferStore persists each committed envelope as EnvelopeRecord { envelope, receipt, created_at } in append-only order.

Committing an entry returns a Receipt { transfer_id } naming the committed envelope. The EnvelopeId is content-addressed (the double-SHA-256 of the canonical envelope bytes), which gives idempotency (re-committing the same envelope returns the cached receipt) and tamper evidence (any edit changes the id).

Kuatia keeps a second append-only sequence, the event log (EventStore of LedgerEvent), for lifecycle notifications and projections. It is not the journal. It records that something happened; the transfer log records exactly which postings moved. See ADR-0010.

Auditability by replay

There are no stored balances to drift. An account's balance is the sum of its Active postings, computed on demand. Re-applying every EnvelopeRecord in order reconstructs all balances exactly. Because each entry is balanced per asset and the log is append-only and content-addressed, the journal is both verifiable (recompute any balance) and tamper-evident (any change to a past entry breaks its id).

Not a journal

Two Kuatia concepts are easy to mistake for the journal and are not:

  • Book is a transfer policy scope. It gates which accounts and assets may participate in a transfer. It is not the journal, not a journal entry, and not a balance partition. See accounting-mapping.md.
  • The event log records lifecycle notifications for subscribers. The transfer log is the source of truth for balances.

See also

  • accounting-mapping.md: full classical double-entry to Kuatia vocabulary mapping.
  • transfers.md: Movement, the resolve algorithm, and the TransferBuilder API.
  • ADR-0013: the decision to model a transfer as a (compound) journal entry and the transfer log as the journal.