π 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
- Simple to understand and implement
- Matches relational DB semantics
- Easy to query current state
β Cons
- No built-in history of changes
- Hard to audit or debug past state
- Schema changes affect data directly
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:
- You need simple, state-based data management
- Your application has no complex domain logic
- You prioritize read/write performance over traceability
- Example: Blog CMS, admin tools, internal dashboards
β Use Event Sourcing When:
- You need auditable state history
- State changes are complex or domain-driven
- You're using CQRS or DDD patterns
- Example: Financial systems, logistics, real-time games
5. ποΈ Event Sourcing Architecture Basics
- Command: User requests a change (
WithdrawMoney
) - Domain logic: Validates and emits events (
MoneyWithdrawn
) - Event Store: Appends event to log
- Projections: Build views (e.g., current balance)
- 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:
- Use versioned event types (e.g.
OrderCreatedV2
) - Use schema migration layers (e.g. adapter functions)
- Keep projections backward-compatible
7. π§ͺ Hybrid: CRUD + Events (Change Data Capture)
Sometimes you can get the best of both:
- Use CRUD for simplicity
- Use CDC (Change Data Capture) tools like Debezium to generate events from DB changes
- Store those in an event store or stream (Kafka, Pulsar)
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:
- Store events immutably in an event store
- Maintain read models (aka projections) that are updated incrementally as new events arrive
- This allows you to read the current state without replaying all events every time
// 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:
- 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
}
-
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
-
Delta Events: Emit derived events like
daily_balance_computed
orinterest_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
- Partition events by aggregate ID (e.g. per user, per account)
- Use Kafka, DynamoDB streams, or purpose-built stores (EventStoreDB, Axon)
2. Asynchronous Projections
- Keep projections separate from the write model
- Use background consumers to update materialized views
- Enables horizontal scaling of read performance
3. Append-Only Optimization
- Use log-structured storage (e.g. Kafka, Apache Pulsar)
- Writes are fast and storage is immutable
4. Snapshotting & Compaction
- Store snapshots for fast recovery
- Apply compaction policies to trim historical events if legally allowed
π¦ 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
- Keep event granularity meaningful β not too coarse or too fine
- Maintain projection rebuild tooling (in case schema evolves)
- Use event versioning practices (additive changes, upcasters)
- Monitor event lag between the event store and projections
<< back to Guides