Yonah Forst
Dec 23, 2018 · 4 min read

About two years ago, I started building an application on top of AWS Lambda. One of the things I struggled with was persisting and sharing data across distinct services and I started looking for the “right” way to do it. I started reading about concepts which I had never heard of: Event sourcing, CQRS, Domain Driven Design. Each of these are complex and fascinating topics in their own right and I encourage you to read more about them.

My goal here is to introduce you to a framework which combines these concepts. I’ll walk you through building a super simple todo list backend using serverless-cqrs. This framework makes it simple to get started and we’ll cover each of them in more depth as we progress.

Let’s jump in!

Events, Actions, Reducers, oh my!

What is Event Sourcing

Storing all the changes (events) to the system, rather than just its current state.

The idea is that given a set of changes (events), you can compute the current state of a system or object. If you’ve ever played around with Redux or another Flux architecture, this should sound familiar.

It flips the traditional database on its head. Normally, the moment something happens in an application, it decides if, and how, this will change the current object and immediately updates the object in the database. With event sourcing, we just store the event that happened. Later, when we go to retrieve the object, we take all those events apply them to get the current state.

You would be right to think that this doesn’t sound very efficient. We’re essentially trading a piece of work that we can do one time now, or many times later. Why not just do it once now!?

This approach definitely has its advantages and disadvantages but it’s not as inefficient as you might think. You can read more about this here but for now, let’s look at an example:

Let’s say we have the following set of events:

const events = [
{ type: 'TodoAdded', title: 'Learn CQRS' },
{ type: 'TodoAdded', title: 'Save the world' },
{ type: 'TodoCompleted', index: 0 },
{ type: 'TodoRemoved', index: 1 },
]

We could write the following reducer to compute the current state:

const todoReducer = (state, event) => {
switch (event.type) {
case 'TodoAdded':
// append event to the list of existing events.
return [
...state,
{ title: event.title, completed: false },
]

case 'TodoRemoved':
// remove the item at given index (non-mutating).
return [
...state.slice(0, event.index),
...state.slice(event.index + 1),
]

case 'TodoCompleted':
// change item at given index (non-mutating).
return [
...state.slice(0, event.index),
{ ...state[event.index], completed: true },
...state.slice(event.index + 1),
]
default:
return state
}
}

Now we can evaluate the events to get our current list of todos:

events.reduce(todoReducer, [])
// [ { title: 'Learn CQRS', completed: true } ]

What just happened?!

  • We took a list of events and ran it through our reducer.
  • The reducer started with an empty array (the initial state: an empty Todo list) and applied each event in order.
  • When it finished, it returned the new state, which is a list with a single completed Todo, ‘Learn CQRS’.

Here’s an embedded environment where you can run this code, try adding some new events and see what happens:

Actions

But where do those events come from? Well, they come from us :) We generate them through functions that we call ‘Actions’. An Action is how we make changes to a state. The Action function takes two arguments: the current state and a payload. Based on those arguments, and ONLY on those arguments, it either reject the command and throw an error, or accepts it and returns one or more event.

Let’s see an example:

const actions = {
addTodo: (state, payload) => {
if (!payload.title) throw new Error('titleMissing')
return [{
type: 'TodoAdded',
title: payload.title,
at: Date.now(),
}]
}
}

We perform very basic validation to make sure that there’s a title and return an array containing the TodoAdded event that we saw earlier.

actions.addTodo([], { foo: 'bar' })
// Error: titleMissing
actions.addTodo([], { title: 'foobar' })
// [ { type: 'TodoAdded', title: 'foobar', at: 1541172900212 } ]

Here’s an embedded environment with the rest of the actions. When you run it, you get a similar list of events to the ones we started with above:

Summary

To summarize, what we have is:

  • A series of functions which, given valid inputs, will produce events describing changes to a domain object.
  • A function which knows how to interpret a list of events to derive the current state.

Together, these functions encapsulate the entire business logic of our domain. They have zero dependencies, are simple to reason about and understand, and are easily testable.

Let’s stop and think about this for a second. We just wrote some code that describes our entire domain, and its rules. There’s no boilerplate code here. Every line of code is specific to the domain.This is our domain and only we know the rules, no one else can write it for us.

Now hold on to your pants: This is (almost) the only code you need to write! How awesome is that?? You don’t need to know anything about where the data is being stored or how it’s going to be accessed. You can focus 100% your domain business logic.

For me, this is a HUGE win. I love that I can describe, and most of all test, my application as series of pure functions. After that, some boilerplate code is inevitable but the less, the better

In the next article, we’ll talk about how to take our Actions and Reducers and turn them into a fully functioning backend.

Thanks for reading!!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade