Skip to main content
Architecture
15 min read

Event Sourcing: When It Works vs When It's Overkill [2025]

Max van Anen - Profile Picture
By
A balanced, real-world analysis of Event Sourcing. Learn to identify when it's the right tool for the job, when it's overkill, and how to make the decision with confidence.
Event SourcingArchitectureCQRSSystem DesignPragmatic Engineering
Event Sourcing: When It Works vs When It's Overkill [2025] - Featured article image for Maxzilla blog

Let's have an honest conversation about Event Sourcing. Not the "Event Sourcing is amazing" conference talk version. Not the "Event Sourcing is evil" rant version. The real version, where we acknowledge it's a powerful tool that's perfect for some problems and terrible for others.

After implementing Event Sourcing in systems processing millions of events daily and also watching teams burn months trying to force it where it doesn't belong, I've learned one thing: the answer to "Should I use Event Sourcing?" is always "It depends."

So let's dig into what it actually depends on.

First, Let's Be Clear About What Event Sourcing Actually Is

Event Sourcing means storing every state change as an immutable event rather than storing current state. Instead of updating a row in a database, you append an event like "UserEmailChanged" or "OrderShipped" to an event log. Current state is derived by replaying these events.

It's not just audit logging. It's not just publishing domain events. It's a fundamental architectural decision that affects every layer of your application.

When Event Sourcing Actually Shines (The Success Stories)

Before we talk about problems, let's acknowledge where Event Sourcing truly excels. These aren't theoretical benefits – these are real wins I've seen in production.

Financial Systems: The Natural Fit

In a payment processing system I worked on, Event Sourcing wasn't just helpful – it was essential. Every transaction naturally maps to events: "PaymentInitiated", "PaymentAuthorized", "PaymentCaptured", "PaymentRefunded". The business literally thinks in events.

PaymentAggregate.kt
// This feels natural and matches the domain perfectly class Payment { fun authorize(amount: Money, cardToken: String): List<Event> { return when (state) { PaymentState.INITIATED -> listOf( PaymentAuthorized( paymentId = id, amount = amount, authorizedAt = Instant.now(), authorizationCode = generateAuthCode() ) ) PaymentState.AUTHORIZED -> throw AlreadyAuthorizedException() PaymentState.CAPTURED -> throw PaymentAlreadyCompletedException() } } fun handle(event: PaymentAuthorized) { state = PaymentState.AUTHORIZED authorizedAmount = event.amount authCode = event.authorizationCode } }

When the auditors come asking "Show me every action taken on payment XYZ-123", you literally hand them the event stream. When a customer disputes a charge, you can replay exactly what happened, in order, with timestamps. It's beautiful when it fits.

Collaborative Systems: Conflict Resolution Done Right

In a collaborative document editor (think Google Docs), Event Sourcing enables something magical: operational transformation. Users generate events like "InsertText", "DeleteRange", "FormatBold". When conflicts occur, you can:

  • Reorder events based on vector clocks
  • Transform operations to maintain consistency
  • Show users exactly who changed what and when
  • Implement "time travel" to see document history

Try implementing this with traditional CRUD. I'll wait.

Complex Business Workflows: Saga Paradise

When you have long-running business processes with compensating transactions, Event Sourcing with the Saga pattern is incredibly powerful:

OrderFulfillmentSaga.kt
@Saga class OrderFulfillmentSaga { @StartSaga @SagaEventHandler fun handle(event: OrderPlaced) { // Reserve inventory commandGateway.send(ReserveInventory(event.orderId, event.items)) } @SagaEventHandler fun handle(event: InventoryReserved) { // Charge payment commandGateway.send(ChargePayment(orderId, event.totalAmount)) } @SagaEventHandler fun handle(event: PaymentFailed) { // Compensate by releasing inventory commandGateway.send(ReleaseInventory(orderId)) } @EndSaga @SagaEventHandler fun handle(event: OrderShipped) { // Saga completed successfully } }

This is orders of magnitude cleaner than managing workflow state in database tables with status columns and timestamp fields.

Where Event Sourcing Becomes a Liability

Now let's talk about where Event Sourcing hurts more than it helps. These aren't edge cases – these are common scenarios where teams waste months fighting the pattern.

Simple CRUD Applications: The Complexity Tax

I watched a team implement Event Sourcing for a basic inventory management system. What should have been a simple "UPDATE products SET quantity = quantity - 1" became:

OverEngineeredInventory.kt
// What used to be one line of SQL class InventoryItem { fun decrementStock(quantity: Int): List<Event> { if (currentStock < quantity) { return listOf(StockDecrementFailed(itemId, quantity, currentStock)) } return listOf(StockDecremented(itemId, quantity, Instant.now())) } fun handle(event: StockDecremented) { currentStock -= event.quantity lastModified = event.timestamp } } // Plus event store, projections, read models, etc. class InventoryProjection { @EventHandler fun on(event: StockDecremented) { // Update read model inventoryRepository.updateStock(event.itemId, -event.quantity) } } // And don't forget handling eventual consistency class InventoryQueryHandler { fun getStock(itemId: String): StockLevel { // Is this the latest? Who knows! return readModelRepository.findById(itemId) ?: eventStore.replayEvents(itemId).calculateStock() } }

For what benefit? They didn't need audit trails. They didn't need time travel. They just needed to track inventory levels. Six months and three developers later, they rewrote it as a simple CRUD app in two weeks.

High-Frequency Updates: The Performance Nightmare

Consider a real-time gaming leaderboard. Players' scores update constantly. With Event Sourcing, every score update is an event:

kotlin
// This generates thousands of events per second per player PlayerScored(playerId: "123", points: 10, timestamp: "2025-01-18T10:00:01") PlayerScored(playerId: "123", points: 5, timestamp: "2025-01-18T10:00:02") PlayerScored(playerId: "123", points: 15, timestamp: "2025-01-18T10:00:03") // ... thousands more // To get current score, you replay ALL events val currentScore = events .filter { it.playerId == "123" } .sumOf { it.points } // Processing thousands of events for one number

Yes, you can implement snapshots. Yes, you can optimize the projections. But why are you fighting the architecture? Just use a counter in Redis and call it a day.

GDPR and Data Privacy: The Immutability Paradox

"Delete my data" says the user. "But our events are immutable!" says Event Sourcing. Welcome to the GDPR nightmare.

The "solutions" are all compromises:

  • Crypto-shredding: Encrypt personal data, delete keys. Hope you never need to debug those events.
  • Event transformation: Rewrite history by filtering events. Breaks the fundamental promise of immutability.
  • Tombstone events: Add "DataDeleted" events. The data is still there, you're just pretending it's not.

Each approach adds complexity and potential security risks. For many applications, it's simpler to just use a database with a DELETE statement.

The Decision Framework: A Practical Guide

Here's my battle-tested framework for deciding whether to use Event Sourcing:

Strong Indicators FOR Event Sourcing:

  • Natural Event-Driven Domain: Your domain experts naturally describe the system in terms of events ("when the order is placed", "after payment is authorized")
  • Audit Requirements: Legal or compliance requirements for complete, immutable audit trails (financial services, healthcare, government)
  • Time Travel is Valuable: You need to reconstruct system state at any point in time for debugging, analysis, or compliance
  • Complex Workflows: Long-running business processes with multiple steps and compensating transactions
  • Collaborative Conflict Resolution: Multiple users modifying the same entities with need for conflict resolution
  • Event-Driven Integration: Your system needs to publish events to other systems anyway

Strong Indicators AGAINST Event Sourcing:

  • Simple CRUD Operations: Your app is mainly forms over data with basic create, read, update, delete operations
  • High-Frequency Updates: The same values change constantly (counters, timestamps, status flags)
  • Large Binary Data: Storing images, documents, or videos as events is usually a terrible idea
  • Team Lacks Experience: Event Sourcing requires a mental model shift. Without experienced team members, you'll likely build it wrong
  • Tight Deadlines: Event Sourcing takes 2-3x longer to implement initially. Not suitable for MVPs or proof-of-concepts
  • GDPR/Privacy Concerns: If you need true data deletion (not just marking as deleted), Event Sourcing makes this much harder

The Pragmatic Middle Ground

Here's what I actually recommend for most teams: Start simple, evolve toward Event Sourcing if needed.

Level 1: Domain Events with CRUD

Keep your traditional database, but publish domain events for important state changes:

PragmaticApproach.kt
@Service class OrderService { @Transactional fun placeOrder(order: Order): Order { // Traditional database save val savedOrder = orderRepository.save(order) // Publish domain event for interested parties eventPublisher.publish( OrderPlaced( orderId = savedOrder.id, customerId = savedOrder.customerId, total = savedOrder.total, items = savedOrder.items ) ) return savedOrder } }

You get event-driven integration and can add audit logging, but you're not fighting eventual consistency for simple queries.

Level 2: Audit Log Pattern

Store events alongside your regular data:

audit_tables.sql
-- Your normal table CREATE TABLE orders ( id UUID PRIMARY KEY, customer_id UUID NOT NULL, status VARCHAR(50) NOT NULL, total DECIMAL(10,2) NOT NULL, updated_at TIMESTAMP NOT NULL ); -- Audit/event table CREATE TABLE order_events ( id UUID PRIMARY KEY, order_id UUID NOT NULL, event_type VARCHAR(100) NOT NULL, event_data JSONB NOT NULL, occurred_at TIMESTAMP NOT NULL, user_id UUID, INDEX idx_order_events_order_id (order_id), INDEX idx_order_events_occurred_at (occurred_at) ); -- Best of both worlds: simple queries and event history SELECT * FROM orders WHERE status = 'PENDING'; -- Fast, simple SELECT * FROM order_events WHERE order_id = ?; -- Full history when needed

Level 3: Selective Event Sourcing

Use Event Sourcing only for aggregates that truly benefit from it:

kotlin
// Event sourced for complex workflows @Aggregate class PaymentProcess { // Full Event Sourcing with all its glory } // Simple CRUD for reference data @Entity class Product { // Just a regular JPA entity } // Hybrid approach for orders @Entity class Order { // State in database // Important events in event store }

This isn't architectural purity, but it's pragmatic and it works.

Migration Strategies: From CRUD to Event Sourcing

If you decide you need Event Sourcing later, here's how to migrate without stopping the world:

The Strangler Fig Pattern

  1. Start capturing events alongside your CRUD operations
  2. Build read models from events, compare with existing data
  3. Gradually switch reads to use event-sourced projections
  4. Finally, switch writes to append events only
  5. Keep the old system as a fallback for a while

The Bounded Context Approach

Start with Event Sourcing in new bounded contexts:

  • New features use Event Sourcing if appropriate
  • Existing features remain CRUD
  • Use anti-corruption layers between contexts
  • Migrate contexts one at a time if beneficial

Real Talk: The Human Cost

Beyond technical considerations, consider the human cost:

The Learning Curve is Real

Event Sourcing requires developers to think differently. Simple bugs become complex debugging sessions. "Why is the read model out of sync?" "Which event handler isn't idempotent?" "Why does replaying events give different results?"

The Bus Factor

If only one or two people on your team truly understand the Event Sourcing implementation, you're one resignation away from a maintenance nightmare. I've seen teams abandon working Event Sourcing systems simply because the architects left and nobody else could maintain it.

The Hiring Challenge

Finding developers with Event Sourcing experience is harder and more expensive than finding developers who know SQL. This is a real cost that compounds over time.

The Verdict: It's Not About Good or Bad

Event Sourcing isn't good or bad. It's a tool. Like any tool, it's excellent for some jobs and terrible for others. The problem isn't Event Sourcing itself – it's applying it without understanding the tradeoffs.

Use Event Sourcing when:

  • Your domain naturally thinks in events
  • You need perfect audit trails
  • You have complex workflows with compensating transactions
  • Time travel debugging would provide significant value
  • Your team has the expertise to build and maintain it

Avoid Event Sourcing when:

  • You're building simple CRUD applications
  • You have high-frequency updates to the same values
  • You need strict data privacy compliance
  • Your team lacks Event Sourcing experience
  • You're under tight deadlines

Consider the middle ground when:

  • You want some benefits without full commitment
  • You're not sure if you'll need Event Sourcing later
  • Different parts of your system have different needs
  • You want to experiment and learn

Final Thoughts: Choose Boring Technology (Until You Can't)

My default advice? Start with boring technology. Use PostgreSQL. Use CRUD. Use REST APIs. Add complexity only when the simple solution genuinely doesn't work.

But – and this is important – be ready to recognize when you've hit the limits of simple solutions. When you find yourself building elaborate audit trail mechanisms on top of CRUD, or when debugging production issues becomes impossible without knowing the sequence of changes, it might be time for Event Sourcing.

The key is matching the solution to the problem, not the other way around. Event Sourcing is powerful medicine – use it when you're actually sick, not because it's trendy.

And please, for the love of all that is holy, stop putting Event Sourcing on your resume if you've only used it in a todo app tutorial. We can tell.

For a practical example of Event Sourcing in production, check out our article on implementing Axon Framework without Axon Server, which shows both the power and complexity of real-world event sourcing.