DEV Community

Cover image for Event Sourcing Is Just Reduce Over Time
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

Event Sourcing Is Just Reduce Over Time

Event Sourcing has a reputation problem.

The moment someone mentions it, the conversation usually becomes:

Event Store
CQRS
Snapshots
Replay
Projections
Distributed Systems
Enter fullscreen mode Exit fullscreen mode

And most developers immediately think:

This sounds complicated.
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Most systems store the current state.

When money is deposited:

125
↓
175
Enter fullscreen mode Exit fullscreen mode

The old value disappears.

The database simply stores:

{
  accountId: 1,
  balance: 175
}
Enter fullscreen mode Exit fullscreen mode

Simple.

But something important is lost.


What Actually Happened?

Imagine an auditor asks:

How did we reach 175?
Enter fullscreen mode Exit fullscreen mode

The database cannot answer.

It only knows:

Current State
Enter fullscreen mode Exit fullscreen mode

The history is gone.


Event Sourcing Stores Events Instead

Instead of storing:

{
  balance: 175
}
Enter fullscreen mode Exit fullscreen mode

we store:

[
  {
    type: "DEPOSIT",
    amount: 100
  },
  {
    type: "DEPOSIT",
    amount: 100
  },
  {
    type: "WITHDRAW",
    amount: 25
  }
]
Enter fullscreen mode Exit fullscreen mode

Notice something.

We no longer store state.

We store:

History
Enter fullscreen mode Exit fullscreen mode

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
  )
Enter fullscreen mode Exit fullscreen mode

Output:

175
Enter fullscreen mode Exit fullscreen mode

Look carefully.

That is Event Sourcing.

Literally.


The Formula Behind Everything

Every Event Sourced system follows:

Initial State
+
Event
=
Next State
Enter fullscreen mode Exit fullscreen mode

Which should feel familiar.

Because that's exactly how reducers work.

(state, event) =>
  nextState
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Actions:

[
  { type: "INCREMENT" },
  { type: "INCREMENT" },
  { type: "INCREMENT" }
]
Enter fullscreen mode Exit fullscreen mode

Process them:

actions.reduce(
  reducer,
  0
)
Enter fullscreen mode Exit fullscreen mode

Output:

3
Enter fullscreen mode Exit fullscreen mode

Sound familiar?

It should.

Because Redux and Event Sourcing are siblings.

Both are:

State Evolution
Through Events
Enter fullscreen mode Exit fullscreen mode

Why Event Sourcing Exists

The obvious question:

Why not just store the state?
Enter fullscreen mode Exit fullscreen mode

Good question.

The answer is:

History has value.
Enter fullscreen mode Exit fullscreen mode

Benefit #1: Audit Trail

Traditional database:

Balance = 175
Enter fullscreen mode Exit fullscreen mode

Event Sourcing:

Deposit 100
Deposit 100
Withdraw 25
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

Done.


Benefit #3: Debugging

Imagine a production bug.

Traditional system:

State is wrong.
Enter fullscreen mode Exit fullscreen mode

Why?

Nobody knows.

Event Sourcing:

Replay the events.
Enter fullscreen mode Exit fullscreen mode

The bug becomes reproducible.


Benefit #4: Rebuild Everything

Imagine:

New Reporting System
Enter fullscreen mode Exit fullscreen mode

needs historical data.

Traditional database:

Too late.
Enter fullscreen mode Exit fullscreen mode

Event Sourcing:

Replay events.
Generate reports.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Example:

const ordersByCustomer =
  (state, event) => {
    if (
      event.type ===
      "ORDER_CREATED"
    ) {
      state[
        event.customerId
      ] ??= []

      state[
        event.customerId
      ].push(event)
    }

    return state
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That's the heart of CQRS.

The terminology is harder than the concept.


Event Sourcing Meets RxJS

In the previous article we discussed:

scan()
Enter fullscreen mode Exit fullscreen mode

Remember:

scan()
=
reduce()
+
time
Enter fullscreen mode Exit fullscreen mode

Now imagine events arriving continuously.

Deposit 100

(wait)

Withdraw 25

(wait)

Deposit 50
Enter fullscreen mode Exit fullscreen mode

Suddenly:

events$.pipe(
  scan(
    accountReducer,
    0
  )
)
Enter fullscreen mode Exit fullscreen mode

becomes Event Sourcing in real time.


Real World Example: Shopping Cart

Events:

Add Item
Add Item
Remove Item
Enter fullscreen mode Exit fullscreen mode

Reducer:

(cart, event) =>
  nextCart
Enter fullscreen mode Exit fullscreen mode

Replay events.

Get cart state.

Event Sourcing.


Real World Example: Inventory

Events:

Stock Added
Stock Removed
Stock Returned
Enter fullscreen mode Exit fullscreen mode

Reducer:

(inventory, event) =>
  nextInventory
Enter fullscreen mode Exit fullscreen mode

Replay.

Current stock.

Event Sourcing.


Real World Example: User Activity

Events:

Login
Purchase
Logout
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Instead of here:

events.reduce(
  reducer,
  initialState
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Which is:

(state, event) =>
  nextState
Enter fullscreen mode Exit fullscreen mode

Which is:

Reduce
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)