<< back to Guides

πŸ”„ Guide: CRUD Systems vs Event Sourcing

Modern applications can manage state using CRUD (Create, Read, Update, Delete) operations or via Event Sourcing, where changes are captured as a sequence of immutable events. This guide compares both approaches, their strengths, weaknesses, and when to use them.


1. πŸ“‹ CRUD: The Traditional Model

In a CRUD system, you interact directly with current state stored in a database. It’s the default pattern in relational systems.

πŸ”§ Typical CRUD Operations

Action SQL Example
Create INSERT INTO orders (...)
Read SELECT * FROM orders WHERE id = 1
Update UPDATE orders SET status='shipped'
Delete DELETE FROM orders WHERE id = 1
-- Example: CRUD update
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

βœ… Pros

❌ Cons


2. πŸ“¦ Event Sourcing: State as a Log of Changes

In Event Sourcing, you never store the current state directly. Instead, you store immutable events that describe what happened, and derive state by replaying them.

🧾 Example Events

[
  { "type": "AccountCreated", "data": { "id": 1, "balance": 0 } },
  { "type": "MoneyDeposited", "data": { "id": 1, "amount": 100 } },
  { "type": "MoneyWithdrawn", "data": { "id": 1, "amount": 40 } }
]

πŸ”„ Derived State

At any time, current balance is calculated by replaying events:

// Simplified balance projection
let balance = 0;
for (event of eventStream) {
  if (event.type === 'MoneyDeposited') balance += event.data.amount;
  if (event.type === 'MoneyWithdrawn') balance -= event.data.amount;
}

3. πŸ” CRUD vs Event Sourcing: Side-by-Side

Feature CRUD Event Sourcing
Stores current state βœ… Yes ❌ No β€” state is derived
Stores history ❌ No (unless added manually) βœ… Yes β€” full history by default
Schema flexibility 🟑 Medium (migration required) 🟒 High (event evolution possible)
Read performance 🟒 Fast 🟑 Depends on snapshotting
Write logic 🟒 Simple πŸ”΄ More complex
Debugging ❌ Hard to reconstruct state βœ… Easy to replay and audit
CQRS support ❌ Limited βœ… Common pairing
Transactions βœ… Native (SQL) 🟑 Requires careful coordination

4. πŸ“š Use Cases for Each

βœ… Use CRUD When:

βœ… Use Event Sourcing When:


5. πŸ—οΈ Event Sourcing Architecture Basics

  1. Command: User requests a change (WithdrawMoney)
  2. Domain logic: Validates and emits events (MoneyWithdrawn)
  3. Event Store: Appends event to log
  4. Projections: Build views (e.g., current balance)
  5. Read Models: Optimized views for UI or APIs
POST /withdraw
{
  "accountId": 1,
  "amount": 100
}

=> Emit: { "type": "MoneyWithdrawn", "data": { ... } }

6. πŸ” Versioning & Event Evolution

Events are immutable, but your schema and logic will change. Strategies:


7. πŸ§ͺ Hybrid: CRUD + Events (Change Data Capture)

Sometimes you can get the best of both:


8. 🧠 Summary Comparison Table

Dimension CRUD Event Sourcing
Simplicity βœ… Simple ❌ Complex initial setup
Flexibility 🟑 Limited schema evolution βœ… Evolving event model
Auditability ❌ Manual audit logs βœ… Built-in
Rollback ❌ Hard βœ… Replay past events
Storage βœ… Compact (current only) ❌ Grows over time
Read Model Custom ❌ Shared schema βœ… Any projection logic

πŸ“š Further Reading


❓ FAQ: Event Sourcing vs CRUD


🟑 When the current state is derived from the latest event (e.g., user is verified), do we still need to compute the full event history?

Not necessarily. Many systems only need the current state, which can be stored as a projection or materialized view.

πŸ”Ή Typical approach:

// Example: User status projection
user_id: 123
status: "verified"
updated_at: "2025-06-10"

This works well for low-complexity state (e.g., "verified", "active", "locked").


🟑 But what about use cases like computing account balances from all financial transactions?

In these cases, replaying all events (e.g., deposit, withdrawal) may be required to compute the current state (e.g., balance).

πŸ”Ή Strategies to optimize:

  1. Snapshots: Save periodic state snapshots (e.g., every 1,000 events)
    • Replay only events after the last snapshot
    • Tradeoff: storage overhead, snapshot versioning
// Snapshot example:
snapshot: {
  user_id: 123,
  balance: 3500,
  event_offset: 10593
}
  1. Materialized Views: Maintain balance in real-time using a read model

    • Event handler updates balance as events arrive
    • Fast reads, consistent as of last event processed
  2. Delta Events: Emit derived events like daily_balance_computed or interest_applied

    • Reduces need to compute complex history on-the-fly

🟒 Which approach should I use?

Case Recommended Strategy
Simple status (e.g. verified) Projection or read model
Financial balance Snapshots + projection
Compliance/auditing required Full event history + periodic snapshot
Complex aggregations Event replay + cached materializations

βš™οΈ How do you scale event sourcing systems?

1. Sharding the Event Store

2. Asynchronous Projections

3. Append-Only Optimization

4. Snapshotting & Compaction


πŸ“¦ How does event sourcing compare to CRUD in terms of storage?

Aspect CRUD Event Sourcing
Storage Use Stores only current state Stores full event history + projections
Mutation Type Overwrites previous state Appends immutable events
Auditing Needs extra logging layer Built-in audit trail
Storage Growth Controlled Grows over time, needs pruning/snapshot

🧠 Final Tips


<< back to Guides