What is event sourcing?

Free preview 8 min to read

A bank that can't explain itself

Your bank account shows $2,847.63. You expected $3,347.63. You call the bank and ask where the $500 went. The bank says: "We don't know. We only store your current balance."

That would be terrifying, and yet it's how most software works.

A traditional Rails CRUD application stores the current state of every record. When a user updates their profile, you overwrite the old data. When an order goes from pending to shipped, you run order.update!(status: :shipped), and the previous state, pending, is gone. If someone asks "when did this order ship?" or "who changed it?", you have no answer unless you built a separate audit log.

Event sourcing turns this around. Instead of storing the current state, you store every change that ever happened, as a sequence of immutable events. The current state isn't stored at all: it's derived by replaying those events.


State-based vs event-based: two mental models

Take a simple shopping cart in a traditional Rails app:

# CRUD approach — store current state
cart = Cart.find(42)
cart.items << Item.new(product_id: 7, quantity: 2)
cart.save!

# Later...
cart.items.find_by(product_id: 7).update!(quantity: 3)

# Even later...
cart.items.find_by(product_id: 7).destroy!

After these operations, the database shows an empty cart. What happened along the way (an item added, its quantity changed, the item removed) is lost.

Now the same scenario with event sourcing:

# Event-sourced approach — store every change
event_store.append(ItemAddedToCart.new(
  cart_id: 42, product_id: 7, quantity: 2
))

event_store.append(CartItemQuantityChanged.new(
  cart_id: 42, product_id: 7, new_quantity: 3
))

event_store.append(ItemRemovedFromCart.new(
  cart_id: 42, product_id: 7
))

The cart is still empty, but now you know why. You can answer "What did the customer almost buy?", "When did they change their mind?" and "How often do customers remove items after changing the quantity?" CRUD can't answer these questions. Event sourcing answers them easily.

flowchart LR
    subgraph CRUD["CRUD (State-Based)"]
        direction TB
        C1["cart_items table"]
        C2["Current State: (empty)"]
        C3["Audit: What happened? No idea."]
        C1 --> C2 --> C3
    end

    subgraph ES["Event Sourcing"]
        direction TB
        E1["events stream"]
        E2["1. ItemAdded"]
        E3["2. QtyChanged"]
        E4["3. ItemRemoved"]
        E5["Audit: Full history preserved."]
        E1 --> E2 --> E3 --> E4 --> E5
    end

The accounting ledger analogy

Event sourcing isn't new. Accountants have worked this way for over 700 years.

In double-entry bookkeeping, you never erase a ledger entry. If you made a mistake, you add a correcting entry. The current balance is always the sum of all entries, so you can audit every transaction and rebuild the balance at any point in time.

Event sourcing does the same for software:

# Accounting ledger (event-sourced by nature)
class LedgerEntry
  # date, description, debit, credit
end

balance = entries.sum { |e| e.credit - e.debit }

# Software event store (same principle)
class AccountEvent
  # timestamp, event_type, data
end

current_state = events.reduce(initial_state) do |state, event|
  apply(state, event)
end

One phrase sums it up: "the log is the truth." The events are the authoritative record. Everything else (the current balance, the cart contents, the order status) is a derived view you can rebuild at any time.


How this connects to Rails

Rails is built around ActiveRecord, which is state-based by nature: you call save! and the row is overwritten. Event sourcing doesn't mean giving up Rails, though. It means modeling certain parts of your domain differently.

Here is a minimal event sourcing pattern in a Rails app:

# app/models/event.rb
class Event < ApplicationRecord
  # table: events
  # columns: id, stream_name, event_type,
  #          data (jsonb), metadata (jsonb),
  #          created_at

  validates :stream_name, :event_type, presence: true

  scope :for_stream, ->(name) { where(stream_name: name) }
  scope :ordered, -> { order(:id) }
end

# Publishing an event
Event.create!(
  stream_name: "Order-#{order_id}",
  event_type: "OrderPlaced",
  data: { customer_id: 42, total: 99.99 },
  metadata: { actor_id: current_user.id }
)

# Rebuilding current state
events = Event.for_stream("Order-#{order_id}").ordered
order_state = events.reduce({}) do |state, event|
  case event.event_type
  when "OrderPlaced"
    state.merge(status: :placed, total: event.data["total"])
  when "OrderShipped"
    state.merge(status: :shipped, shipped_at: event.created_at)
  when "OrderCancelled"
    state.merge(status: :cancelled)
  else
    state
  end
end

This isn't production-grade event sourcing; gems like rails_event_store provide the real infrastructure. It does show the core idea: append events, derive state.


When event sourcing is essential

Event sourcing earns its complexity when your domain has these traits.

Audit is non-negotiable

Financial systems, healthcare records, legal documents. Regulators ask "show me every change that led to this state." CRUD with an audit log is fragile, because the log can drift away from reality. With event sourcing, the events are the reality.

Questions about the past matter

"What plan was this customer on on March 15th?" In CRUD, you hope someone built a versions table. With event sourcing, you replay the events up to that date.

Business insight depends on behavior over time

Cart abandonment analysis, user journey mapping, fraud detection. All of them need to know what happened, not only what is.

Several people edit the same thing

Google Docs, Figma, multiplayer games: several users change the same entity at once. Events (operations) can be merged and replayed; overwritten state can't.


When event sourcing is overkill

Here's what event sourcing advocates rarely say out loud: most applications shouldn't use it.

Scenario Event sourcing? Why
Blog with posts and comments No CRUD is simpler and enough. No audit need.
User profile management No Overwriting a name is fine. Nobody needs the history.
Internal admin dashboard No Low complexity, few audit requirements.
E-commerce order pipeline Yes Audit trail, questions about the past, complex state changes.
Financial transactions Yes The law requires a complete history.
Collaborative document editor Yes Concurrent edits need the history of operations.
Simple CRUD LMS enrollment No A boolean completed flag is enough.
XP ledger in a gamified LMS Yes Immutable, append-only records. A natural fit.

The Enrollment::ExperiencePoint model in this LMS is event-sourced in spirit. Each XP award is an immutable record, and the total is derived by summing them. You never update an XP record; you only append new ones. That's event sourcing without the full infrastructure. One caveat: nothing in the model forbids an update, so "immutable" is a convention here. Overriding readonly? to return persisted? would turn it into a rule.


The trade-off matrix

Event sourcing isn't free. This is what you gain and what you pay:

Dimension CRUD Event sourcing
Storage Compact: one row per entity Grows without limit: every change is kept
Read speed Fast: query the current state directly Slower without projections: you must replay
Write speed Moderate: read, change, write Fast: append-only, though concurrent writes to one stream need optimistic locking
Audit trail Needs a separate implementation Built in: the events are the audit trail
Debugging "What is the state?" is easy; "How did it get here?" is hard The event log answers both
Complexity Low: well understood and native to Rails High: projections, eventual consistency, event versioning
Team skills Standard Rails knowledge A different way of thinking for the whole team
Schema changes Migrations change existing data Events are immutable, so evolving the schema is harder

Teams underestimate the organizational cost most of all. Event sourcing changes how developers think about data. If your ten Rails developers have never worked with it, expect a real slowdown while they adjust, often measured in months.


Common mistakes

Event-sourcing the whole application

Ninety percent of your app is simple CRUD: settings pages, admin panels, user profiles. Wrapping those in events adds ceremony and gives nothing back.

Instead, event-source only the domains where history, audit or questions about the past matter. Keep the rest as standard Rails CRUD.

Treating event sourcing as "just an audit log"

An audit log is a secondary record of what happened. In event sourcing, events are the primary record and the current-state table is secondary. Mix up those roles and your events and your state will drift apart.

Instead, make the events the source of truth. Derive all state from events, never the other way round.

Storing events nobody reads

Events nobody reads are dead weight: storage and complexity for nothing.

Instead, before you create an event type, name at least one real consumer that needs it: a projection, a report, a notification.


Summary

Concept What to remember
CRUD (state-based) Stores the current state only; history is lost on every update
Event sourcing Stores every change as an immutable event; the current state is derived
The log is the truth Events are the authoritative record; state tables are derived views
Accounting ledger Bookkeeping has worked this way for 700+ years
When to use it Audit trails, financial systems, questions about the past, collaborative editing
When to avoid it Simple CRUD, admin panels, user profiles, blogs
Main trade-off You gain full history and audit; you pay in complexity and storage
Organizational cost It changes how the team thinks; expect months of lower productivity
Lesson 1 of 7 · free

Keep going with lesson 2

Events as first-class citizens

Covers past-tense event names, schemas holding values from the moment, metadata, immutability and three strategies for versioning events.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...