Building for Auditability from Day One
A system can have logs, dashboards and approval workflows and still be unable to answer an auditor. Logging and auditability are different things, and the difference has to be designed in at the start.
Logging is not auditability
Most teams, when asked whether their system is auditable, point to their logs. Application logs. Access logs. Error logs. A dashboard showing payment totals. And to be fair, those things have real value. But when an auditor sits across from you and asks why a $2M purchase order was approved on a Tuesday afternoon three months ago, application logs will not save you.
Auditors ask three questions. What happened? When did it happen? Who approved it, and what did they see when they approved it? Most systems built without auditability in mind can answer the first question adequately. They struggle with the second. And they completely fail the third.
Logging records outcomes. Auditability reconstructs decisions. An outcome is: payment of $450,000 was sent to Vendor ID 8821 on March 14th. A decision is: at 14:23 on March 12th, the finance director saw a purchase order for $450,000 with supporting quote #Q-2024-8821-3, reviewed it against budget line 4420, and clicked approve. Those are different data. Systems built to record outcomes almost never capture the second type, and that is what auditors need.
This is easy to get wrong. You can have good logging and a clear approval UI and still be recording state changes instead of decisions. The difference only shows when an auditor asks for something you cannot produce.
What reconstruction looks like
Say a regional distributor finds a $2M discrepancy at quarter-end: a series of AP transactions that don't match their goods receipts. Nothing looks fraudulent. It could be duplicate processing, a migration artifact, or legitimate payments to a vendor that was restructured under a new entity. The auditors need to know which.
The ERP has an approval workflow, user accounts and permission roles. It stores the current state of every invoice in a normalized relational database, the way most enterprise systems do. What it lacks is a record of what each invoice looked like at the moment of approval. When an invoice is revised after approval, the record is updated in place. The approval timestamp remains. The data it referred to is gone.
Reconstructing what an approver saw then means cross-referencing application logs with gaps in them, email archives, PDF attachments in an unstructured document store, and the memories of staff. That is weeks of work for several people, for what should have been a two-day query.
The cause is mutable state with no event history. The fix is simple at design time and close to impossible to retrofit without rebuilding the data layer.
Mutable state vs. immutable events
Standard relational database design records the current state of an entity. An invoice row has a status column. When the invoice is approved, the status updates from PENDING to APPROVED. The previous status is gone. This is efficient for querying current state. It is a disaster for auditing.
Event sourcing inverts this. Instead of storing the current state of an entity, you store every event that has ever happened to it. The current state is derived by replaying the event log. An invoice has no status field to update. It has an event stream: InvoiceCreated, InvoiceLineItemAdded, InvoiceSubmittedForApproval, InvoiceApproved. Each event is immutable once written. You can always reconstruct what the invoice looked like at any point in time because you have a complete, ordered history of every change.
You do not need full event sourcing to get most of the audit benefit. The minimum viable approach is append-only tables for anything that requires auditability: a separate table that records every state transition, who triggered it, what the entity looked like at that moment (serialized as JSON), and a timestamp. Never delete rows. Never update rows. The current state table can be as mutable as you like. The audit table is its permanent record.
Our take
Correlation IDs and why you cannot retrofit them
This is the one that causes the most pain when discovered late. A correlation ID is a unique identifier assigned at the start of a business transaction, for example when a document enters the system, and passed through every downstream service, database write and log entry the transaction touches.
Without correlation IDs, tracing a single business event across a multi-service system requires timestamp matching, educated guessing, and manual cross-referencing. With them, you run one query: give me everything that touched correlation ID txn_20240314_8821_x9k and you get every database write, service call and approval event in that transaction, in order.
The reason you cannot retrofit them: correlation IDs need to be present in every table, every log format, every service payload from the start. Adding them to a production system that has been running for two years means modifying dozens of database schemas, updating every service that writes to those tables, and still having no correlation IDs on historical data, which is exactly the data auditors ask about.
We treat a missing correlation ID strategy as a blocker before any financial system build starts.
Every enterprise financial system should be designed to answer an auditor's question before that auditor ever arrives. If you are building auditability after the first audit request, you have already failed the design review. The bill just hasn't arrived yet.
E-invoicing rules are making this the law
In Saudi Arabia this is already law. ZATCA, the Zakat, Tax and Customs Authority, requires e-invoicing under Phase 2 of its Fatoora program. Every invoice carries a UUID, a cryptographic stamp and a hash of the invoice before it, so the chain cannot be quietly rewritten.
A business already running append-only event logs with correlation IDs has most of what it needs. It adds the signing layer and the API integration. A business on a mutable-state ERP has to buy a compliance middleware layer or rebuild its invoicing data model.
Egypt already mandates e-invoicing, Jordan has its national JoFotara system, and the UAE is rolling out its own. If you are building a financial system for a business in the region today, assume rules like these will reach it. Build for them now and adapting costs little. Wait, and you face the same choice between middleware and a rebuild.
Watch out
What auditability-first architecture looks like in practice
None of it is complicated. The decisions just have to be made at the start.
Every entity that participates in a financial or compliance workflow gets an append-only event table alongside its standard state table. The event table records: event type, entity ID, actor ID (human or system), the full serialized state of the entity at that moment, a correlation ID linking it to the originating transaction, and a server-side timestamp. No application code is permitted to update or delete rows in event tables. This is enforced at the database level with row-level permissions, not just at the application level by convention.
Decision logs are kept separate from action logs. When a human approves a payment, the decision log records the word "approved" and also the data snapshot they approved: the invoice amount, the vendor, the budget line, the supporting documents referenced, and the approval screen version they were looking at. This means that even if the invoice is later revised or the supporting document is replaced, the record of what the approver saw is permanent.
Correlation IDs are generated at the entry point of every business workflow (document ingestion, manual data entry, an API call from an external system) and propagated through every service boundary in the request context. They appear in every database write, every log line, and every outbound API call for the duration of that transaction.
The cost at design time is a couple of days of schema planning and some extra implementation time to instrument everything. The cost of skipping it is weeks of reconstruction during an audit, which is the worst possible time to pay it.
Related reading