Why an Audit Log Isn't Event Sourcing — And What Changes When You Store Intent
We've been separating reads and writes. Now we go deeper: what if the write model didn't store current state at all? What if it stored every decision that led to the current state?
The bug report said: "Order #4471 shows a total of €0.00 but the customer was charged €847.50."
It was a Thursday afternoon on a large-scale insurance platform. The order record in the database showed zero. The payment gateway confirmed €847.50 was collected. Somewhere between the payment confirmation and the final database update, the total had been overwritten. But overwritten by what? When? By which process?
We had logging. We had an audit trail — a separate table that recorded "who changed what, when." We queried it. The audit trail showed the order total changing from €847.50 to €0.00 at 14:23:07. The modified_by column said system. The reason column was null.
Three engineers spent two days reconstructing what happened. The root cause turned out to be a race condition: a background recalculation job running concurrently with a manual discount application. The recalculation picked up a stale snapshot, computed the total without the line items (which were mid-transaction), and wrote €0.00. The audit log told us that it changed. It didn't tell us why.
If the system had stored the sequence of events — OrderCreated, ItemAdded, ItemAdded, DiscountApplied, PaymentConfirmed — instead of repeatedly overwriting a single row, the bug would have been visible in minutes. Not because the events prevent race conditions. But because they make the history legible. Every decision, every state transition, preserved in order.
That's the difference between an audit log and event sourcing. And it's the pattern we're building this week.
Stratigraphic layers
In 1840, the British geologist William Smith published the first geological map of an entire country. His insight wasn't about rocks — it was about time. Each layer of sediment represents a period. The deeper you dig, the further back you go. The surface tells you what exists now. The layers tell you how it got there.
Archaeologists adopted the same principle. When they excavate a site, they don't care about the final state of the ground — the parking lot, the field, the building. They care about the strata: the layers of occupation, destruction, rebuilding, and abandonment stacked on top of each other. Each layer is an event. The sequence of layers is the history.
Traditional databases work like bulldozers. They flatten the site to show you the current surface. You know what's there now. You don't know what was there before, what was demolished, what was built on top of what. The audit log is a notebook sitting next to the bulldozer — someone jotted down what happened, but the actual layers are gone.
Event sourcing preserves the strata.
What event sourcing actually is
Event sourcing is a persistence strategy where the source of truth is not the current state of an entity, but the ordered sequence of events that produced that state.
Instead of this:
// Traditional: store current state
public class Order
{
public Guid Id { get; set; }
public decimal Total { get; set; } // overwritten on every change
public string Status { get; set; } // overwritten on every change
public DateTime LastModified { get; set; } // overwritten on every change
}You store this:
// Event sourcing: store what happened
public record OrderCreated(Guid OrderId, Guid CustomerId, DateTime CreatedAt);
public record ItemAdded(Guid OrderId, Guid ProductId, int Quantity, decimal Price);
public record DiscountApplied(Guid OrderId, decimal Percentage, string Reason);
public record OrderConfirmed(Guid OrderId, DateTime ConfirmedAt);
public record OrderCancelled(Guid OrderId, string Reason, DateTime CancelledAt);The current state of Order #4471 is computed by replaying these events in order. Not stored. Computed. The events are the truth. The state is a derivative.
public class Order
{
public Guid Id { get; private set; }
public decimal Total { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<IDomainEvent> _events = new();
public static Order Rehydrate(IEnumerable<IDomainEvent> history)
{
var order = new Order();
foreach (var evt in history)
order.Apply(evt);
return order;
}
private void Apply(IDomainEvent evt)
{
switch (evt)
{
case OrderCreated e:
Id = e.OrderId;
Status = OrderStatus.Created;
break;
case ItemAdded e:
Total += e.Quantity * e.Price;
break;
case DiscountApplied e:
Total *= (1 - e.Percentage / 100);
break;
case OrderConfirmed _:
Status = OrderStatus.Confirmed;
break;
case OrderCancelled _:
Status = OrderStatus.Cancelled;
break;
}
}
}This is a fundamental shift. In a traditional system, the database row is the order. In an event-sourced system, the database stores the story of the order. The row is just one possible reading of that story.
What event sourcing is not
Three misconceptions I encounter regularly:
"It's CQRS." No. CQRS separates read and write models. Event sourcing changes how the write model persists. They compose naturally — event sourcing produces the events that CQRS projections consume — but they're independent patterns. You can have CQRS without event sourcing (as we built in the CQRS chapters), and you can have event sourcing without CQRS (though you'll probably want projections for querying).
"It's an audit log." An audit log records that something changed. Event sourcing records what the intent was. DiscountApplied(OrderId, 15%, "loyalty program") carries business meaning. An audit row saying total changed from 100 to 85 by system does not. The events are the domain language, not a generic changelog.
"It means you never delete data." Event sourcing stores all events, yes. But GDPR, data retention policies, and storage costs mean you need strategies for handling old data. Crypto-shredding (encrypting personal data in events with a key you can delete), event compaction, and archival are real concerns. Append-only doesn't mean append-forever.
The bridge from CQRS
Last week we built a CQRS system where commands produced domain events and projections consumed them. The write side used EF Core to persist the current state of the order. The events were a side effect — published after the state was saved.
Event sourcing flips this relationship. The events are the persistence. The current state is the side effect — derived by replaying events. The projection pipeline we built for CQRS still works. But now the events aren't ephemeral messages that disappear after handling. They're the permanent, immutable record of everything that happened.
The command handler from the CQRS blueprint chapter changes in one critical way:
// CQRS (explored in the previous chapters): save state, then publish events
await _writeDb.Orders.AddAsync(order);
await _writeDb.SaveChangesAsync();
foreach (var evt in order.DomainEvents)
await _mediator.Publish(evt);
// Event sourcing (this week): append events, state is derived
await _eventStore.AppendAsync(
streamId: $"order-{order.Id}",
events: order.DomainEvents,
expectedVersion: order.Version);One line replaces three. But that one line carries a different set of trade-offs.
The trade-offs nobody skips
You gain: Complete history. Temporal queries ("what was the order total at 2pm Tuesday?"). Perfect audit trail with business intent. The ability to replay events to build new read models retroactively. Debugging that shows exactly what happened, in what order, with what intent.
You pay: Aggregate loading requires replaying all events (mitigated by snapshots, but that's added complexity). Event schema versioning becomes critical — you can't ALTER TABLE on an event that happened two years ago. Storage grows monotonically. Every developer on the team must understand event-driven thinking, which is a steeper learning curve than "load entity, modify, save."
Event sourcing doesn't add complexity to your system. It moves complexity from "figuring out what happened" to "designing what you record." Whether that's a good trade depends on how often you need to answer the question: how did we get here?
The insurance platform bug — Order #4471, €0.00, two days of investigation — would have been diagnosed in the time it takes to query a stream. Event sourcing doesn't prevent bugs — it makes every bug's history visible.
There's something quietly powerful about a system that never forgets. Not because forgetting is wrong, but because the questions you'll need to answer in six months are ones you can't predict today.
The strata are the truth. The surface is just the latest reading.




