Skip to content
Bitloops - Give your AI agents high-signal context in milliseconds.
HomeAbout us
DocsBlog
ResourcesSoftware DesignEvent Sourcing: State from History

Event Sourcing: State from History

Event sourcing inverts traditional databases: instead of storing current state, you store every state change. It's powerful for auditability and temporal queries, but you pay for it in complexity. Use it only when the benefits outweigh the cost.

9 min readUpdated March 4, 2026Software Design

What Event Sourcing Actually Is

Traditional databases store state: current values in tables. A customer record has a name, email, and address. When data changes, you update the record. The old values are gone.

Event sourcing inverts this. Instead of storing state, you store events: the facts about what happened. State is computed from those events.

// Traditional approach: store state
const customer = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  address: '123 Main St'
};

// Later, update the state
customer.email = 'alice.new@example.com';
// Old email is lost

// Event sourcing: store history
const events = [
  { type: 'CustomerCreated', id: 1, name: 'Alice', email: 'alice@example.com', address: '123 Main St' },
  { type: 'EmailChanged', id: 1, newEmail: 'alice.new@example.com' }
];

// State = aggregation of all events
function getCurrentState(events) {
  let state = {};
  for (let event of events) {
    if (event.type === 'CustomerCreated') {
      state = event;
    } else if (event.type === 'EmailChanged') {
      state.email = event.newEmail;
    }
  }
  return state;
}

// Result: state = { id: 1, name: 'Alice', email: 'alice.new@example.com', address: '123 Main St' }
javascript

Event sourcing is an append-only log of what happened. State is derived from this log, not stored directly.

Why This Matters

Complete Audit Trail

Every change is recorded. Regulators, compliance officers, and support teams can see exactly what happened, when, and in what order.

// Query: "Show me everything that happened to this customer"
const events = eventStore.getEventsFor(customerId);

events.forEach(event => {
  console.log(`${event.timestamp}: ${event.type} - ${JSON.stringify(event.data)}`);
});

// Output:
// 2024-01-15T10:00:00Z: CustomerCreated - {name: 'Alice', email: 'alice@example.com'}
// 2024-02-01T14:30:00Z: EmailChanged - {newEmail: 'alice.new@example.com'}
// 2024-02-05T09:15:00Z: AddressChanged - {newAddress: '456 Oak Ave'}
javascript

Traditional databases give you a snapshot. Event sourcing gives you the entire story.

Temporal Queries

Ask questions about the past. "What was this customer's email on February 1st?" "How many active orders did we have last month?"

// Reconstruct state at a point in time
function getStateAsOf(customerId, targetDate) {
  const events = eventStore.getEventsFor(customerId);
  let state = {};

  for (let event of events) {
    if (event.timestamp > targetDate) break; // Stop before target date

    if (event.type === 'CustomerCreated') {
      state = event.data;
    } else if (event.type === 'EmailChanged') {
      state.email = event.data.newEmail;
    }
  }

  return state;
}

const feb1State = getStateAsOf(customerId, new Date('2024-02-01'));
console.log(feb1State.email); // 'alice@example.com' (before the change)
javascript

Debugging and Root Cause Analysis

When something goes wrong, you have the complete history. Why is this order in a weird state? Check the events. Was there a bug? Was it user action? You can replay the exact sequence.

// Replay to understand what happened
const events = eventStore.getEventsFor(orderId);
let order = new Order();

for (let event of events) {
  console.log(`Applying: ${event.type} at ${event.timestamp}`);
  order.applyEvent(event);
  console.log(`Order state: ${order.status}`);
}

// You see exactly what led to the current state
javascript

Event-Driven Integration

Other systems subscribe to events. When something changes, interested parties react.

// Order system publishes OrderPlaced event
const event = new OrderPlacedEvent(order.id, order.customerId, order.total);
eventBus.publish(event);

// Billing system listens
class BillingEventHandler {
  onOrderPlaced(event) {
    const invoice = new Invoice(event.orderId, event.total);
    invoiceRepository.save(invoice);
  }
}

// Inventory system listens
class InventoryEventHandler {
  onOrderPlaced(event) {
    // Allocate stock
  }
}

// Email system listens
class EmailEventHandler {
  onOrderPlaced(event) {
    // Send confirmation
  }
}

// No coupling. Each system reacts independently.
javascript

Scalability

Event store is append-only. Appends are fast, even at huge scale. Reading is replaying events, which is parallelizable.

How to Implement Event Sourcing

The Event Store

An append-only log of immutable events.

class EventStore {
  constructor(database) {
    this.db = database;
  }

  append(event) {
    // Events are immutable - just insert
    this.db.insert('events', {
      aggregateId: event.aggregateId,
      aggregateType: event.aggregateType,
      type: event.constructor.name,
      data: JSON.stringify(event),
      timestamp: new Date(),
      version: this.getNextVersion(event.aggregateId)
    });

    // Publish event to subscribers
    eventBus.publish(event);
  }

  getEvents(aggregateId) {
    return this.db.query(
      'SELECT * FROM events WHERE aggregateId = ? ORDER BY version',
      [aggregateId]
    );
  }

  getAllEvents(since = null) {
    const query = 'SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp';
    return this.db.query(query, [since || new Date(0)]);
  }

  getNextVersion(aggregateId) {
    const result = this.db.query(
      'SELECT COUNT(*) as count FROM events WHERE aggregateId = ?',
      [aggregateId]
    );
    return result[0].count + 1;
  }
}
javascript

Events as First-Class Citizens

Define events clearly. They're your domain language.

class OrderPlacedEvent {
  constructor(orderId, customerId, items, total, timestamp = new Date()) {
    this.aggregateId = orderId;
    this.aggregateType = 'Order';
    this.orderId = orderId;
    this.customerId = customerId;
    this.items = items; // [{productId, quantity, price}]
    this.total = total;
    this.timestamp = timestamp;
  }
}

class OrderShippedEvent {
  constructor(orderId, trackingNumber, shippingDate, timestamp = new Date()) {
    this.aggregateId = orderId;
    this.aggregateType = 'Order';
    this.orderId = orderId;
    this.trackingNumber = trackingNumber;
    this.shippingDate = shippingDate;
    this.timestamp = timestamp;
  }
}

class OrderDeliveredEvent {
  constructor(orderId, deliveryDate, timestamp = new Date()) {
    this.aggregateId = orderId;
    this.aggregateType = 'Order';
    this.orderId = orderId;
    this.deliveryDate = deliveryDate;
    this.timestamp = timestamp;
  }
}
javascript

Reconstructing State

Aggregates reconstruct themselves from events.

class Order {
  static fromEvents(events) {
    const order = new Order();
    for (let event of events) {
      order.applyEvent(event);
    }
    return order;
  }

  constructor() {
    this.id = null;
    this.customerId = null;
    this.items = [];
    this.total = null;
    this.status = null;
    this.trackingNumber = null;
  }

  applyEvent(event) {
    if (event instanceof OrderPlacedEvent) {
      this.id = event.orderId;
      this.customerId = event.customerId;
      this.items = event.items;
      this.total = event.total;
      this.status = 'placed';
    } else if (event instanceof OrderShippedEvent) {
      this.status = 'shipped';
      this.trackingNumber = event.trackingNumber;
    } else if (event instanceof OrderDeliveredEvent) {
      this.status = 'delivered';
    }
  }
}

// Load aggregate from history
const events = eventStore.getEvents(orderId);
const order = Order.fromEvents(events);
javascript

Projections: Building Read Models

While events are the source of truth, you don't want to replay events for every query. Projections build views from events.

class OrderListProjection {
  constructor(readDatabase) {
    this.db = readDatabase;
  }

  projectOrderPlacedEvent(event) {
    // Build a view optimized for listing orders
    this.db.insert('order_list', {
      id: event.orderId,
      customerId: event.customerId,
      status: 'placed',
      total: event.total,
      createdAt: event.timestamp
    });
  }

  projectOrderShippedEvent(event) {
    this.db.update(
      'order_list',
      { status: 'shipped', trackingNumber: event.trackingNumber },
      { id: event.orderId }
    );
  }

  projectOrderDeliveredEvent(event) {
    this.db.update(
      'order_list',
      { status: 'delivered', deliveredAt: event.timestamp },
      { id: event.orderId }
    );
  }
}

class OrderDetailProjection {
  constructor(readDatabase) {
    this.db = readDatabase;
  }

  projectOrderPlacedEvent(event) {
    this.db.insert('order_detail', {
      id: event.orderId,
      customerId: event.customerId,
      items: JSON.stringify(event.items),
      total: event.total,
      status: 'placed',
      events: JSON.stringify([event])
    });
  }

  projectOrderShippedEvent(event) {
    const detail = this.db.query('SELECT * FROM order_detail WHERE id = ?', [event.orderId])[0];
    const events = JSON.parse(detail.events);
    events.push(event);

    this.db.update(
      'order_detail',
      { status: 'shipped', events: JSON.stringify(events) },
      { id: event.orderId }
    );
  }
  // ...
}

// Event handler subscribes to events and updates projections
class EventProjector {
  constructor(projections) {
    this.projections = projections;
  }

  handle(event) {
    for (let projection of this.projections) {
      const methodName = `project${event.constructor.name}`;
      if (projection[methodName]) {
        projection[methodName](event);
      }
    }
  }
}

// Event bus publishes to projector
eventBus.subscribe(event => {
  eventProjector.handle(event);
});
javascript

Snapshots: Optimization

Replaying 10,000 events every time you load an aggregate is slow. Snapshots cache state at intervals.

class SnapshotStore {
  constructor(database) {
    this.db = database;
    this.snapshotInterval = 100; // Snapshot every 100 events
  }

  saveSnapshot(aggregateId, state, version) {
    this.db.insert('snapshots', {
      aggregateId,
      aggregateType: state.constructor.name,
      state: JSON.stringify(state),
      version,
      timestamp: new Date()
    });
  }

  getLatestSnapshot(aggregateId) {
    const results = this.db.query(
      'SELECT * FROM snapshots WHERE aggregateId = ? ORDER BY version DESC LIMIT 1',
      [aggregateId]
    );
    return results[0] || null;
  }
}

// Load with snapshots
class OptimizedEventStore {
  constructor(eventStore, snapshotStore) {
    this.eventStore = eventStore;
    this.snapshots = snapshotStore;
  }

  loadAggregate(aggregateId) {
    // Try to load from snapshot
    const snapshot = this.snapshots.getLatestSnapshot(aggregateId);

    let state;
    let startVersion = 0;

    if (snapshot) {
      state = JSON.parse(snapshot.state);
      startVersion = snapshot.version;
    }

    // Load and replay only events after snapshot
    const events = this.eventStore.getEvents(aggregateId);
    const remainingEvents = events.filter(e => e.version > startVersion);

    for (let event of remainingEvents) {
      state.applyEvent(event);
    }

    // Save new snapshot if we've accumulated enough events
    if (remainingEvents.length > this.snapshots.snapshotInterval) {
      this.snapshots.saveSnapshot(aggregateId, state, events.length);
    }

    return state;
  }
}
javascript

The Real Costs

Schema Evolution

Events are immutable. Once written, they stay as-is. When your event schema changes, you need strategies for handling old events.

// Old event format
{ type: 'EmailChanged', email: 'new@example.com' }

// New event format
{ type: 'EmailChanged', email: 'new@example.com', reason: 'user request' }

// When replaying old events, the 'reason' is missing
// Aggregates must handle this gracefully

class Customer {
  applyEmailChangedEvent(event) {
    this.email = event.email;
    this.emailChangeReason = event.reason || 'unknown'; // Handle missing field
  }
}
javascript

Storage

You're storing every change ever made. That's a lot of data. Archival strategies are needed.

Complexity

Event sourcing is hard. It requires careful thinking about state reconstruction, eventual consistency, and event ordering.

Distributed Systems Challenges

If your event store spans multiple nodes, you need consensus on event ordering. This is non-trivial.

When Event Sourcing Makes Sense

Audit-Critical Systems

Banking, compliance, healthcare: you need complete history. Event sourcing is natural.

Complex Domain Logic

When state transitions are complicated, events make them explicit and testable.

Event-Driven Architectures

When many systems need to react to changes, event sourcing produces the events naturally.

Temporal Queries

When you frequently ask "what was this like at time T?" event sourcing is built for that.

When Event Sourcing Is Overkill

Simple Databases

Todo apps, simple CRUD tools. Traditional databases are simpler.

High-Volume Writes

If you're writing millions of events per second, the overhead of event sourcing might be too much.

No Audit Requirements

If you don't need history, event sourcing adds complexity without benefit.

FAQ

Doesn't event sourcing use a lot of storage?

Yes, more than storing just current state. Compression, archival, and careful schema design help. Use snapshots for frequently accessed aggregates.

What if an event is incorrect?

You can't delete events (immutability). Instead, publish a correction event that negates the bad one, or a new event that represents the true state.

How do I handle concurrent writes?

Use optimistic concurrency control. Version each event. If someone writes event version 5 when version 6 exists, reject it and retry.

Can I use event sourcing without CQRS?

Yes. But they pair well together—events are your write model, projections are your read models.

What happens if my event handler crashes?

Handlers must be idempotent (safe to run twice). Mark events as processed only after successful handling. Retry on failure.

Primary Sources

  • Martin Fowler's explanation of event sourcing patterns and their architectural implications. Event Sourcing
  • Greg Young's comprehensive guide to CQRS principles and event-driven architecture. CQRS Documents
  • Foundational text on domain models and ubiquitous language for event-driven systems. Domain-Driven Design
  • Practical implementation guide for domain-driven design and event sourcing patterns. Implementing DDD
  • Patterns for organizing event-driven and message-based application architectures. EAA Patterns
  • Refactoring and evolutionary design techniques for event-sourced systems. Refactoring

Get Started with Bitloops.

Apply what you learn in these hubs to real AI-assisted delivery workflows with shared context, traceable reasoning, and architecture-aware engineering practices.

curl -sSL https://bitloops.com/install.sh | bash