Why Event Sourcing?
Understanding when event sourcing solves real problems and when it doesn't
Let’s be honest: event sourcing isn’t a silver bullet. It’s a tool that solves specific problems really well, but it’s not right for every application. Here’s when it makes sense and when it doesn’t.
Problems event sourcing actually solves
Section titled “Problems event sourcing actually solves”Audit requirements
Section titled “Audit requirements”Your app needs to track who did what, when, and why.
Traditional approach: Add audit tables, hope you capture everything, pray you never need to debug what happened.
-- Traditional audit tableCREATE TABLE audit_log ( id SERIAL PRIMARY KEY, table_name VARCHAR(50), operation VARCHAR(10), old_values JSONB, new_values JSONB, user_id INT, created_at TIMESTAMP);
-- Good luck figuring out the business logic from this
Event sourcing approach: Every business action is an event. The audit trail is the data.
// Every business action is explicitly modeledconst events = [ { type: 'order.placed', data: { orderId: '123', customerId: 'abc', items: [...] } }, { type: 'payment.authorized', data: { orderId: '123', amount: 99.99 } }, { type: 'order.shipped', data: { orderId: '123', trackingNumber: 'xyz' } }, { type: 'order.cancelled', data: { orderId: '123', reason: 'customer request' } }];
// The audit trail tells a story
Temporal queries
Section titled “Temporal queries”You need to answer questions like “What did this look like yesterday?” or “Show me the state before the bug was introduced.”
Traditional approach: Hope you have backups. Maybe add version columns to everything. Pray it’s enough.
Event sourcing approach: Time travel is built-in.
// What was this user's status before the suspension?const userBeforeSuspension = await getUserAtTime(userId, '2024-01-15T10:30:00Z');
// Show me all orders that were pending when the system went downconst pendingOrders = await getOrdersAtTime('2024-01-15T14:22:00Z', 'pending');
Complex business rules
Section titled “Complex business rules”Your domain has intricate rules that change frequently, and you need to be able to prove compliance.
Traditional approach: Business logic scattered across services, stored procedures, and that one Excel macro the business team maintains.
Event sourcing approach: Business rules are explicit in event handlers. Want to change the rule? Add a new event type.
// Evolution of business rules over timeclass OrderPricingRules { calculatePrice(events: OrderEvent[]): number { let price = 0;
for (const event of events) { switch (event.type) { case 'item.added': price += event.data.price; break;
case 'discount.applied': // Rule change: After 2024-01-01, discounts can't exceed 50% const maxDiscount = event.createdAt > '2024-01-01' ? 0.5 : 1.0; const discount = Math.min(event.data.percentage, maxDiscount); price *= (1 - discount); break;
case 'tax.calculated': // Rule change: New tax rates for certain states const taxRate = this.getTaxRate(event.data.state, event.createdAt); price += price * taxRate; break; } }
return price; }}
Integration hell
Section titled “Integration hell”You’re building microservices and need reliable communication between them.
Traditional approach: API calls everywhere. Hope services are available. Deal with partial failures. Implement complex retry logic.
Event sourcing approach: Services communicate through events. Eventual consistency is explicit, not accidental.
// Order service publishes eventsawait eventStore.appendToStream('order-123', [ { type: 'order.placed', data: { customerId: 'abc', total: 99.99 } }]);
// Inventory service reactseventBus.subscribe('order.placed', async (event) => { await reserveInventory(event.data.items);});
// Payment service reactseventBus.subscribe('order.placed', async (event) => { await authorizePayment(event.data.customerId, event.data.total);});
// If payment fails, inventory automatically gets releasedeventBus.subscribe('payment.failed', async (event) => { await releaseInventory(event.data.orderId);});
When NOT to use event sourcing
Section titled “When NOT to use event sourcing”Simple CRUD applications
Section titled “Simple CRUD applications”If your app is mostly forms that save to database tables, event sourcing is overkill.
Just use Rails/Django/Laravel:
- User registration? INSERT into users table.
- Profile update? UPDATE users SET name = ?
- Delete account? DELETE FROM users WHERE id = ?
No events needed. No complexity added.
Read-heavy applications
Section titled “Read-heavy applications”If you’re building a CMS, blog, or content site where 99% of operations are reads, event sourcing adds unnecessary complexity.
Traditional approach is fine:
SELECT title, content, author FROM posts WHERE published = true ORDER BY created_at DESC;
Small teams without domain expertise
Section titled “Small teams without domain expertise”Event sourcing requires understanding distributed systems, eventual consistency, and complex debugging. If your team is learning web development, start simpler.
Start with: Database → API → Frontend Graduate to: Event sourcing when you hit the problems it solves
Performance-critical applications
Section titled “Performance-critical applications”Event sourcing adds latency. You’re writing events, then building read models from events. If you need sub-millisecond response times, traditional databases are often faster.
The honest trade-offs
Section titled “The honest trade-offs”What you gain
Section titled “What you gain”- Complete audit trail: Every change is recorded
- Time travel: Query any point in history
- Flexible integration: Services communicate via events
- Debugging superpowers: Replay scenarios to understand bugs
- Compliance: Prove what happened and when
What you lose
Section titled “What you lose”- Simplicity: More moving parts than CRUD
- Immediate consistency: Read models might be slightly behind
- Query complexity: Building views from events takes work
- Learning curve: Your team needs to understand the patterns
What you need to handle
Section titled “What you need to handle”- Eventually consistent reads: Your UI needs to handle this
- Event versioning: Events schemas will change over time
- Operational complexity: More infrastructure to monitor
- Development speed: Initial features take longer to build
When to make the switch
Section titled “When to make the switch”Start with event sourcing if:
Section titled “Start with event sourcing if:”- You’re building financial systems
- Audit trails are legally required
- You have complex, evolving business rules
- You’re doing microservices and struggling with data consistency
- You need to answer temporal queries
Add event sourcing later if:
Section titled “Add event sourcing later if:”- Your application starts hitting audit requirements
- You need better microservice integration
- Traditional approaches become too complex
- You need to debug complex business scenarios
Stay with traditional databases if:
Section titled “Stay with traditional databases if:”- Your domain is simple and stable
- Performance is critical
- Your team is small and learning
- You’re building a content site or simple CRUD app
The DeltaBase difference
Section titled “The DeltaBase difference”Most event sourcing solutions require you to:
- Set up complex infrastructure (Kafka, EventStore, etc.)
- Manage consistency across multiple databases
- Handle event versioning and migration
- Build operational tooling for monitoring
DeltaBase removes these barriers:
- Zero infrastructure: Runs on Cloudflare’s edge
- Built-in consistency: ACID transactions for events
- Automatic scaling: Handles traffic spikes transparently
- Developer-friendly: TypeScript SDK, not configuration files
Event sourcing doesn’t have to be complex. It just needs to solve the right problems for your application.