Event Sourcing has a reputation problem.
The moment someone mentions it, the conversation usually becomes:
Event Store
CQRS
Snapshots
Replay
Projections
Distributed Systems
And most developers immediately think:
This sounds complicated.
I used to think the same thing.
Then one day I realized something.
The core idea behind Event Sourcing is not complicated at all.
In fact, most JavaScript developers already know it.
They just know it by a different name.
reduce()
That's right.
The architecture behind some of the largest systems in the world can be explained using a function most developers learn in their first year of JavaScript.
Because Event Sourcing is really just:
Reduce applied to business events.
And once you see that, everything starts making sense.
The Traditional Way We Store State
Suppose we have a bank account.
Current balance:
{
accountId: 1,
balance: 125
}
Most systems store the current state.
When money is deposited:
125
↓
175
The old value disappears.
The database simply stores:
{
accountId: 1,
balance: 175
}
Simple.
But something important is lost.
What Actually Happened?
Imagine an auditor asks:
How did we reach 175?
The database cannot answer.
It only knows:
Current State
The history is gone.
Event Sourcing Stores Events Instead
Instead of storing:
{
balance: 175
}
we store:
[
{
type: "DEPOSIT",
amount: 100
},
{
type: "DEPOSIT",
amount: 100
},
{
type: "WITHDRAW",
amount: 25
}
]
Notice something.
We no longer store state.
We store:
History
Reconstructing State
Now let's calculate the balance.
const accountReducer =
(balance, event) => {
switch (event.type) {
case "DEPOSIT":
return balance + event.amount
case "WITHDRAW":
return balance - event.amount
default:
return balance
}
}
const balance =
events.reduce(
accountReducer,
0
)
Output:
175
Look carefully.
That is Event Sourcing.
Literally.
The Formula Behind Everything
Every Event Sourced system follows:
Initial State
+
Event
=
Next State
Which should feel familiar.
Because that's exactly how reducers work.
(state, event) =>
nextState
Same idea.
Different scale.
Redux Was Preparing You For This
Consider Redux.
const reducer = (
state,
action
) => {
switch(action.type) {
case "INCREMENT":
return state + 1
default:
return state
}
}
Actions:
[
{ type: "INCREMENT" },
{ type: "INCREMENT" },
{ type: "INCREMENT" }
]
Process them:
actions.reduce(
reducer,
0
)
Output:
3
Sound familiar?
It should.
Because Redux and Event Sourcing are siblings.
Both are:
State Evolution
Through Events
Why Event Sourcing Exists
The obvious question:
Why not just store the state?
Good question.
The answer is:
History has value.
Benefit #1: Audit Trail
Traditional database:
Balance = 175
Event Sourcing:
Deposit 100
Deposit 100
Withdraw 25
Now you know exactly how the state was produced.
Benefit #2: Time Travel
Want the account balance yesterday?
Simply replay events until yesterday.
eventsUntilYesterday.reduce(
reducer,
0
)
Done.
Benefit #3: Debugging
Imagine a production bug.
Traditional system:
State is wrong.
Why?
Nobody knows.
Event Sourcing:
Replay the events.
The bug becomes reproducible.
Benefit #4: Rebuild Everything
Imagine:
New Reporting System
needs historical data.
Traditional database:
Too late.
Event Sourcing:
Replay events.
Generate reports.
History becomes reusable.
Projections Are Just More Reducers
This is another concept that sounds scary.
A projection is simply:
A reducer that creates a view.
Example:
const ordersByCustomer =
(state, event) => {
if (
event.type ===
"ORDER_CREATED"
) {
state[
event.customerId
] ??= []
state[
event.customerId
].push(event)
}
return state
}
Process events.
Build a customer report.
That's a projection.
CQRS Starts Making Sense
Most CQRS explanations make it sound mysterious.
Reality:
Commands
↓
Generate Events
↓
Reducers
↓
State
That's the heart of CQRS.
The terminology is harder than the concept.
Event Sourcing Meets RxJS
In the previous article we discussed:
scan()
Remember:
scan()
=
reduce()
+
time
Now imagine events arriving continuously.
Deposit 100
(wait)
Withdraw 25
(wait)
Deposit 50
Suddenly:
events$.pipe(
scan(
accountReducer,
0
)
)
becomes Event Sourcing in real time.
Real World Example: Shopping Cart
Events:
Add Item
Add Item
Remove Item
Reducer:
(cart, event) =>
nextCart
Replay events.
Get cart state.
Event Sourcing.
Real World Example: Inventory
Events:
Stock Added
Stock Removed
Stock Returned
Reducer:
(inventory, event) =>
nextInventory
Replay.
Current stock.
Event Sourcing.
Real World Example: User Activity
Events:
Login
Purchase
Logout
Store events forever.
Build analytics later.
No extra tracking tables required.
Why Event Sourcing Feels Difficult
Because people usually start here:
Event Store
CQRS
Snapshots
Distributed Systems
Instead of here:
events.reduce(
reducer,
initialState
)
The second explanation is far easier.
Because that's the core idea.
Everything else is optimization.
The Problem With Event Sourcing
Let's be fair.
Event Sourcing is not free.
1. More Complexity
A CRUD application is usually simpler.
2. Data Migration Becomes Harder
Events are history.
Changing history is dangerous.
3. Event Design Matters
Poorly designed events become technical debt.
4. Learning Curve
Teams must understand the model.
5. Not Every System Needs It
A simple blog probably doesn't need Event Sourcing.
Pros Of Event Sourcing
1. Complete Audit History
Nothing is lost.
2. Time Travel
State at any point in time.
3. Easier Debugging
Replay events.
Reproduce bugs.
4. Flexible Reporting
Build projections later.
5. Natural Fit For Distributed Systems
Events scale surprisingly well.
The Real Lesson
The biggest lesson I learned about Event Sourcing was that it wasn't really about architecture.
It was about a pattern I already knew.
Current State
+
Event
=
Next State
Which is:
(state, event) =>
nextState
Which is:
Reduce
At its core, Event Sourcing is simply reducing business events into state.
The architecture becomes much less intimidating once you realize that.
And suddenly concepts like:
CQRS
Projections
Replay
Snapshots
start feeling like natural extensions rather than mysterious enterprise magic.
What's Next?
In the next article we'll discuss:
Why Functional Code Can Be Slower
Because after spending several articles discussing elegant abstractions, it's time to address a controversial reality:
Beautiful code
≠
Fast code
And understanding that tradeoff makes you a much better engineer.
About The Author
Hi, I'm Amrish Khan.
I enjoy building developer tools, exploring software architecture, and writing about the deeper ideas behind everyday programming concepts.
I'm also building Aruvix — a growing ecosystem of local-first developer tools designed to process data directly in the browser without unnecessary uploads.
Here's a detailed blog on Aruvix:
https://dev.to/amrishkhan05/aruvix-the-ultimate-offline-first-developer-toolkit-e0i
You can follow my work and thoughts here:
Portfolio:
https://www.amrishkhan.dev
LinkedIn:
https://www.linkedin.com/in/amrishkhan
GitHub:
https://www.github.com/amrishkhan05
If you enjoyed this article, consider following for more deep dives into JavaScript, architecture, local-first software, and performance engineering.
Top comments (0)