What is event sourcing?
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 |
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.