kuatia-storage (EventStore, LedgerEvent), kuatia (saga, ledger)The transfer log is the append-only source of truth for value (ADR-0001):
balances are projected by summing Active postings, and nothing else is
authoritative. But applications need to react to the ledger (update a read
model, notify a downstream service, drive an outbox), and not everything worth
reacting to is a value transfer: accounts are also created, frozen, unfrozen
and closed. Polling the transfer log misses the lifecycle events and forces
every consumer to re-derive "what changed." Do we need a second log, what is
authoritative, and what are its delivery/idempotency semantics?
Derive all reactions from the append-only transfer records.
Pros:
Cons:
Make an event stream authoritative and fold balances from events.
Pros:
Cons:
Active postings, and the UTXO/posting
model would be demoted to a projection of the event log.Keep the transfer log authoritative. Add an EventStore: LedgerEvent { seq,
timestamp, kind } where kind is TransferCommitted { transfer_id } or an
account-lifecycle event. The store assigns a monotonic seq and exposes
append_event + get_events_since(after_seq, limit). append_event is
store-side idempotent on event_dedup_key: replayable events
(TransferCommitted, re-driven by saga recovery) dedup on the transfer id and
return the existing seq; events with no natural identity (account lifecycle)
return None and may recur. The feed is derived: it observes what the
authoritative writes already decided.
Pros:
get_events_since) instead of
re-deriving from transfers.TransferCommitted survive saga
recovery's re-drive without emitting a duplicate: at-least-once upstream
becomes effectively-once for the events that carry a content identity.Cons:
seq, and
retain.append_event deviates from the dumb-storage "return an
affected-row count" rule (ADR-0003): the store assigns seq and performs the
dedup, since both are storage-native and the key is content-based, not a
state-machine decision. A deliberate, narrow exception.seq orders events but is not a causal/transactional clock; it
is an emission order, not a serialization of value state.Chosen option: Option 3, a derived, append-only event stream alongside the
authoritative transfer log, because it gives applications a uniform, ordered
feed (including non-transfer lifecycle events) without challenging ADR-0001's
"transfer log is the only authority on value." Making append_event idempotent
on a content-based event_dedup_key is what lets the saga emit it safely under
recovery re-drive; accepting that keyless lifecycle events may recur keeps the
model honest about at-least-once delivery. The store-side seq assignment and
dedup are a consciously scoped exception to dumb storage (ADR-0003), justified
because both are intrinsic storage concerns rather than domain decisions.
get_events_since) for read models, outboxes and
notifications; consumers no longer reverse-engineer the transfer table.TransferCommitted is effectively-once thanks to transfer-id dedup, aligning
with the saga's idempotent re-drive (ADR-0003) and content-addressed
transfers.Active postings; the event stream adds
no competing authority.append_event is a documented exception to the count-returning storage
contract.seq is emission order, not a causal clock.append_event exception
is scoped against that ADR's dumb-storage contract.crates/kuatia-storage/src/events.rs (LedgerEvent,
event_dedup_key, EventStore).