Decisions · ADR-003
Fail-closed audit transaction
Audit-log write and business mutation share one database transaction. Audit-write failure rolls back the mutation. No silent state changes.
Context
A platform that records compliance state changes asynchronously creates a window where the business state diverges from the audit log. If the business mutation succeeds and the audit-log write fails, the audit trail is incomplete, and the divergence is undetectable from the inside. If the audit write succeeds and the business mutation fails, the audit log records a state change that never happened. Either failure mode invalidates the integrity story this platform sells.
Decision
Every state-changing operation binds the business mutation and the audit-log write into a single database transaction. If either fails, both roll back. There is no path through the codebase where a determination is made but unrecorded, or recorded but unmade. The pattern is enforced via a withAuditTransaction(...) wrapper that every state-changing route handler calls; lint rules forbid direct database mutations outside the wrapper.
Consequences
The audit log is structurally consistent with business state. Slight cost: transaction lock duration grows because the audit write is part of the critical section. Operations that fan out to multiple business mutations need to coordinate at the route layer (one transaction per route, not one per mutation). The pattern is uniform; engineers know that “mutation equals transactional audit write” is the only way a state change ships.
Alternatives considered
- Async audit logging via a message queue. Rejected: introduces a window where business state succeeds but audit lags. Replay of a queue failure cannot reconstruct intent.
- Best-effort audit with retry on failure. Rejected: “best-effort” means “fail-open.” Fail-open is the failure mode this decision exists to prevent.
References
- standard ACID (Atomicity, Consistency, Isolation, Durability) atomicity guarantees
- standard Two-phase commit patterns for distributed audit logs (not used here; single-database transaction is sufficient)