ScaledByDesign/Insights
ServicesPricingAboutContact
Book a Call
Scaled By Design

Fractional CTO + execution partner for revenue-critical systems.

Company

  • About
  • Services
  • Contact

Resources

  • Insights
  • Pricing
  • FAQ

Legal

  • Privacy Policy
  • Terms of Service

© 2026 ScaledByDesign. All rights reserved.

contact@scaledbydesign.com

On This Page

The Promise and the PainCRUD vs Event SourcingWhen Event Sourcing Is Worth ItA Practical ImplementationProjections (Read Models)The Hard Parts
  1. Insights
  2. Architecture
  3. Event Sourcing — When It's Worth the Complexity

Event Sourcing — When It's Worth the Complexity

June 22, 2026·ScaledByDesign·
event-sourcingarchitecturecqrsdesign-patternsdatabases

The Promise and the Pain

Event sourcing stores every change as an immutable event instead of overwriting the current state. Instead of a balance: 500 column, you store: deposited 1000, withdrew 300, deposited 100, withdrew 300. The current state is derived by replaying events.

The promise: complete audit trail, time-travel queries, decoupled systems. The pain: eventual consistency, complex queries, event versioning, and a team that needs to think differently about data.

CRUD vs Event Sourcing

CRUD (traditional):
  UPDATE accounts SET balance = balance - 100 WHERE id = 123;
  
  → Current state: balance = 500
  → What happened before? No idea (unless you add audit logging)
  → Can we undo? Only if we track what changed

Event Sourcing:
  events: [
    { type: "AccountOpened", amount: 0, at: "2026-01-01" },
    { type: "MoneyDeposited", amount: 1000, at: "2026-01-15" },
    { type: "MoneyWithdrawn", amount: 300, at: "2026-02-01" },
    { type: "MoneyDeposited", amount: 100, at: "2026-02-15" },
    { type: "MoneyWithdrawn", amount: 300, at: "2026-03-01" },
  ]
  
  → Current state: replay events → balance = 500
  → What happened before? Everything is recorded
  → Can we undo? Apply a compensating event

When Event Sourcing Is Worth It

Strong fit:
  ✓ Financial systems (audit trail is legally required)
  ✓ Order management (need to know every state change)
  ✓ Collaborative editing (multiple users changing same data)
  ✓ Compliance-heavy domains (healthcare, banking, government)
  ✓ Systems that need temporal queries ("what was the state on March 1?")
  ✓ Complex business workflows with many state transitions

Not worth the complexity:
  ✗ Simple CRUD applications (user profiles, settings)
  ✗ Content management systems
  ✗ Analytics dashboards (read-heavy, no write concerns)
  ✗ Prototypes and MVPs (you'll rebuild anyway)
  ✗ Small teams without event-driven experience

A Practical Implementation

// Event definitions
type OrderEvent =
  | { type: "OrderCreated"; orderId: string; customerId: string; items: OrderItem[] }
  | { type: "PaymentReceived"; amount: number; method: string }
  | { type: "OrderShipped"; trackingNumber: string; carrier: string }
  | { type: "ItemReturned"; itemId: string; reason: string }
  | { type: "RefundIssued"; amount: number; reason: string }
  | { type: "OrderCanceled"; reason: string };
 
// Event store
interface EventStore {
  append(streamId: string, events: DomainEvent[], expectedVersion: number): Promise<void>;
  getEvents(streamId: string, fromVersion?: number): Promise<DomainEvent[]>;
}
 
// Aggregate: rebuild state from events
class OrderAggregate {
  private state: OrderState = { status: "unknown", items: [], payments: [], totalPaid: 0 };
  private version = 0;
 
  static async load(eventStore: EventStore, orderId: string): Promise<OrderAggregate> {
    const aggregate = new OrderAggregate();
    const events = await eventStore.getEvents(`order-${orderId}`);
    for (const event of events) {
      aggregate.apply(event);
      aggregate.version++;
    }
    return aggregate;
  }
 
  private apply(event: OrderEvent): void {
    switch (event.type) {
      case "OrderCreated":
        this.state = { status: "created", items: event.items, payments: [], totalPaid: 0 };
        break;
      case "PaymentReceived":
        this.state.payments.push({ amount: event.amount, method: event.method });
        this.state.totalPaid += event.amount;
        this.state.status = "paid";
        break;
      case "OrderShipped":
        this.state.status = "shipped";
        this.state.tracking = { number: event.trackingNumber, carrier: event.carrier };
        break;
      case "OrderCanceled":
        this.state.status = "canceled";
        break;
    }
  }
 
  // Commands produce events (with business rule validation)
  ship(trackingNumber: string, carrier: string): OrderEvent[] {
    if (this.state.status !== "paid") {
      throw new Error("Cannot ship an unpaid order");
    }
    return [{ type: "OrderShipped", trackingNumber, carrier }];
  }
 
  cancel(reason: string): OrderEvent[] {
    if (this.state.status === "shipped") {
      throw new Error("Cannot cancel a shipped order — use returns instead");
    }
    return [{ type: "OrderCanceled", reason }];
  }
}

Projections (Read Models)

Event sourcing separates writes (events) from reads (projections):

// Projection: build a read-optimized view from events
class OrderDashboardProjection {
  async handle(event: OrderEvent & { streamId: string; timestamp: Date }) {
    switch (event.type) {
      case "OrderCreated":
        await db.orderDashboard.create({
          data: {
            orderId: event.orderId,
            customerId: event.customerId,
            status: "created",
            itemCount: event.items.length,
            createdAt: event.timestamp,
          },
        });
        break;
 
      case "PaymentReceived":
        await db.orderDashboard.update({
          where: { orderId: extractOrderId(event.streamId) },
          data: { status: "paid", totalPaid: { increment: event.amount } },
        });
        break;
 
      case "OrderShipped":
        await db.orderDashboard.update({
          where: { orderId: extractOrderId(event.streamId) },
          data: { status: "shipped", trackingNumber: event.trackingNumber },
        });
        break;
    }
  }
}

The Hard Parts

Event versioning:
  → Events are immutable. You can't change old events.
  → When the schema changes, you need upcasters:
    v1: { type: "OrderCreated", total: 100 }
    v2: { type: "OrderCreated", total: { amount: 100, currency: "USD" } }
  → Upcaster transforms v1 events to v2 shape on read

Eventual consistency:
  → Projections lag behind events (milliseconds to seconds)
  → Users might not see their change immediately
  → Solution: "read your own writes" pattern

Snapshot optimization:
  → Replaying 10,000 events per aggregate is slow
  → Take periodic snapshots of aggregate state
  → Load from snapshot + replay only newer events

Event sourcing isn't an architecture you should adopt lightly. But for domains where the history of changes is as important as the current state — financial systems, order management, compliance-heavy applications — it provides capabilities that are nearly impossible to retrofit into a CRUD system. Start with one bounded context, keep your events small and focused, and invest in good projections for reads.

Previous
Feature Flags for Backend Services — Beyond the Toggle
Insights
Event Sourcing — When It's Worth the ComplexityAPI Gateway Patterns That Scale — From Simple Proxy to PlatformDomain-Driven Design Without the PhD — A Practical GuideMicroservices Communication Patterns — Sync, Async, and When to Use EachMonorepo vs. Polyrepo — The Decision Framework Nobody Gives YouAPI Versioning Strategies That Don't Become a Maintenance NightmareEvent-Driven Architecture Without the PhD — A Practical GuideCQRS Without the Complexity — A Practical Implementation GuideThe Strangler Fig Migration That Saved a 10-Year-Old MonolithWhy You Should Start With a MonolithEvent-Driven Architecture for the Rest of UsThe Real Cost of Microservices at Your ScaleThe Caching Strategy That Cut Our Client's AWS Bill by 60%API Design Mistakes That Will Haunt You for YearsMulti-Tenant Architecture: The Decisions You Can't UndoCI/CD Pipelines That Actually Make You FasterThe Rate Limiting Strategy That Saved Our Client's APIWhen to Rewrite vs Refactor: The Decision Framework

Ready to Ship?

Let's talk about your engineering challenges and how we can help.

Book a Call