Event Sourcing vs CRUD: Use It for Audit, Not for Aesthetics_
Event sourcing is powerful when history matters, but it adds real complexity. Use it where auditability and replay are core requirements.
Event sourcing gets oversold in two different ways.
One group treats it like the only serious way to model business systems. Another group dismisses it as architecture cosplay. Both are wrong.
Event sourcing is justified when the history of change is part of the business value, not just an implementation detail.
If all you need is a blog CMS or a straightforward admin panel, CRUD is usually the right abstraction. If you are building a ledger, an audit-heavy workflow, or a domain where reconstructing decisions matters, event sourcing becomes a serious option.
The Core Difference
In a CRUD model, the current row is the source of truth.
In an event-sourced model, the source of truth is an append-only stream of domain events, and current state is derived from those events.
That difference sounds small on paper. It changes a lot in practice.
A CRUD update:
UPDATE subscriptions
SET plan = 'pro', updated_at = now()
WHERE id = 'sub_123';
An event-sourced write:
{
"type": "SubscriptionUpgraded",
"subscriptionId": "sub_123",
"previousPlan": "starter",
"newPlan": "pro",
"occurredAt": "2024-03-22T05:23:50Z"
}
The first statement tells you what is true now. The second tells you what happened.
That distinction matters when auditors, support staff, or downstream systems need the sequence, not just the result.
Why Teams Reach for It
Event sourcing earns its complexity when you need some combination of:
- a strong audit trail
- replayability
- temporal queries such as "what did we believe yesterday?"
- explicit domain events for downstream consumers
It is especially attractive when change history is a first-class business concept rather than an afterthought.
Why Teams Regret It
The cost is real:
- you need projections for efficient reads
- schema evolution becomes an ongoing concern
- eventual consistency becomes part of the product experience
- operational debugging gets harder
A CRUD system can often answer a support question with one query. An event-sourced system may require understanding the stream, the projection, and whether the projection is caught up.
That is why event sourcing should be chosen for a business reason, not because it feels more elegant.
The Read Model Is Not Optional
A pure event log is not a good general-purpose read store.
In real systems, event sourcing usually ends up paired with projections or CQRS-style read models:
type SubscriptionProjection = {
id: string;
currentPlan: string;
status: "active" | "canceled";
renewedAt: string | null;
};
async function handle(event: DomainEvent) {
switch (event.type) {
case "SubscriptionUpgraded":
await db.query(
`UPDATE subscription_projection
SET current_plan = $2
WHERE id = $1`,
[event.subscriptionId, event.newPlan],
);
break;
}
}
The event stream preserves history. The projection makes the system usable.
The Hard Parts Nobody Skips
If you adopt event sourcing, plan for these from day one:
Event versioning
Your understanding of the domain will change. Old events still need to be readable.
Idempotent consumers
Handlers must tolerate duplicate delivery or replay.
Projection rebuilds
You need a safe way to rebuild read models from the source stream.
Operational visibility
You need to know whether a projection is fresh, lagging, or broken.
None of that is theoretical. It is the daily operational surface of the system.
A Useful Decision Rule
Use CRUD when:
- current state is what the business mainly cares about
- audit can be handled with conventional logging
- the domain is simple and query-heavy
Consider event sourcing when:
- the sequence of change is itself valuable
- correction and replay are core workflows
- multiple downstream systems depend on domain events
This is not about architectural maturity. It is about fit.
Further Reading
Related Writing.
Continue with closely related articles on software engineering, architecture, and implementation trade-offs.
SQLite Is Production-Ready in the Right Shape of System
SQLite is not just a toy database, but it is also not a universal replacement for PostgreSQL. The workload shape decides whether it fits.
UUIDv4 vs ULID: What Actually Matters for Database Writes
Random identifiers are convenient, but they are not free. If write locality matters, compare UUIDv4 with time-ordered identifiers such as ULID or UUIDv7.