Ledger operations
Invariants (non-negotiable - verify before commit)
- Every transaction balances:
SUM(debits) == SUM(credits)per currency. - One stable idempotency key per ledger operation:
<domain>:<operation>:<business-id>, linked to all of its postings. Retries reuse the same key. - In a service-owned materialized ledger, lock affected accounts in ascending order before mutating balances. Provider-backed ledgers use the provider's concurrency contract.
- Append-only: corrections are new reversing entries. Never UPDATE or DELETE posted rows.
- FX: snapshot the rate onto the posting at post-time; never re-derive it later.
materialized_balance == account-type normal-balance signed SUM(postings)- the reconciliation queries in the references must return zero rows.
Mode detection
Detect which ledger backs the service from its dependencies/config, then read the matching reference before writing code:
- Blnk-backed service →
references/blnk-api-contract.md- hold/inflight semantics,available = balance - inflight, timeout meansPENDING_RECONCILIATION, and duplicate references are accepted only after the original operation is fetched and matched. - pgledger-backed service →
references/pgledger-invariants.md- schema law (append-only trigger,balance_after), sortedFOR UPDATElocking, zero-row verification queries.
Required tests before a ledger change ships
- Balance invariant holds under the new operation.
- Insufficient-funds rejection path.
- Idempotent replay returns the original result - no double posting.
Anti-patterns
- In a service-owned posting schema, signed amounts instead of positive amount plus explicit debit or credit direction.
- In a local materialized ledger, a balance read before mutation without the required lock.
- Retrying a timed-out post with a fresh idempotency key.
- Floats or doubles anywhere near money.
Schema changes to ledger tables follow the migrations skill (append-only trigger stays, no destructive ops).