System DesignHard
Event sourcing vs CRUD — when does the complexity pay off in microservices?
CRUD stores the current state. Event sourcing stores the history of changes; current state is a fold/replay.
Side-by-side
| CRUD | Event sourcing | |
|---|---|---|
| Storage | Latest row | Append-only event log |
| Audit trail | Extra work | Free, complete |
| Read model | Same as write | CQRS — projections |
| Time-travel queries | no | yes |
| Operational complexity | Low | High |
| Schema evolution | ALTER TABLE | Versioned events + upcasters |
When event sourcing pays off
- Regulated domains — banking, healthcare, claims processing. Auditors want every state transition.
- Disputable transactions — order returns, fraud investigation, "who changed this and when".
- Multiple read models — same events power search, analytics, and operational UI.
- Temporal queries — "what did the customer's address look like on 1 March?"
When CRUD wins
- CRUD apps with no audit need. Adding event sourcing here is over-engineering.
- High-frequency mutations of large entities (real-time gaming positions).
- Teams without operational maturity to handle eventual consistency, replay, snapshots.
Sketch — minimal event store
public abstract record OrderEvent(Guid OrderId, DateTime At);
public record OrderPlaced(Guid OrderId, DateTime At, decimal Total) : OrderEvent(OrderId, At);
public record OrderShipped(Guid OrderId, DateTime At, string Tracking) : OrderEvent(OrderId, At);
public class Order {
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public void Apply(OrderEvent e) {
switch (e) {
case OrderPlaced p: Id = p.OrderId; Status = OrderStatus.Placed; break;
case OrderShipped s: Status = OrderStatus.Shipped; break;
}
}
public static Order Replay(IEnumerable<OrderEvent> events) {
var o = new Order(); foreach (var e in events) o.Apply(e); return o;
}
}
Hidden costs nobody warns you about
- Snapshots — replaying 50 000 events per read is unacceptable. Snapshot every N events.
- Event upcasting — when an event schema changes, you cannot rewrite history; you write a translator.
- GDPR right-to-be-forgotten vs immutable log — typically solved with encrypted-payload + key-deletion ("crypto-shredding").
- Read-model rebuild time — production rebuilds can take hours; plan for blue/green projections.
Pragmatic middle ground
Outbox table + change-data-capture. You get the "events" benefit (audit, downstream feeds) without rebuilding aggregates from the log.