Getting started with `serverless-cqrs` — part 2

Yonah Forst
6 min readDec 23, 2018

This is part 2 of a series on building a simple todo backend using serverless-cqrs. If you haven’t read part 1, well, here it is.

In the part 1, we described our domain in terms of Actions (event generators) and Reducers (event interpreters). This is the entire business logic of our application. However, these are implemented as pure functions. Meaning, they can only take some input and return some output, but never mutate data or write to a database.

So what we basically have is a “description” of a domain. There are a few things missing before we can actually use it in an application:

  1. We need a place to store events. i.e. something that lets us retrieve existing events and append new ones. This is called an Adapter (much like a database adapter for an ORM system)
  2. We need a way to take an action and call it on an existing (or new) domain object by passing it just the objects id. To accomplish this, we have something called a Command.

When you execute a command it simply loads all existing events for that object from storage using the adapter, computes the current state, and passes this state to the action you are trying to invoke. The action then either throws an error (because the change would violate your business rules), or succeeds and returns a new event, which is then appended to storage using the same adapter.

Let’s go through this one more time:

  • Events describe changes to your domain objects.
  • An Adapter allows us to retrieve/add events from/to some storage mechanism.
  • A Reducer takes a list of events and turns them into a state.
  • Actions take a state and validate a ‘request to change’. If the change is allowed, the action returns a new event representing that change.
  • Commands tie everything together. They talk with the adapter, the reducer, and the action which we are invoking. They validate our request, and save any new events back to our storage mechanism.

If this doesn’t make sense yet, try reading it one more time :) it can take a few tries before it ‘clicks’.

CQRS

CQRS means “Command-query responsibility segregation”. We segregate the responsibility between commands (write requests) and queries (read requests). The write requests and the read requests are handled by different objects.

Let’s use serverless-cqrs to generate separate objects for reading and writing.

WriteModel

We’re going to call the object that does writing our ‘write model’. For the write model, we need our Actions and Reducer as well as a storage adapter. In the example below, we’ll use an in-memory adapter:

//STEP 1
//load an in-memory adapter builder
const memoryAdapterBuilder = require('serverless-cqrs.memory-adapter')
// build the adapter
const adapter = memoryAdapterBuilder.build({
entityName: 'todo'
})
//STEP 2
// load the domain 'description': the actions and reducer
// from part 1 of this series
const actions = require('./actions)
const reducer = require('./reducer)
//load the write model builder
const { writeModelBuilder } = require('serverless-cqrs')
// and now here's the magic!
// build the write model
const writeModel = writeModelBuilder.build({
actions,
reducer,
adapter,
})

What did we just do? In STEP 1, we initialized our adapter (the object knows how to store events). In STEP 2, we took the actions and the reducer from part 1, along with the adapter we just initialized and fed them to the writeModelBuilder from serverless-cqrs.

We now have a write model which accepts commands as described by our actions.

const id = '123' //this is the id of our todo list)
writeModel.addTodo(id, { title: 'foobar' })
// returns true
writeModel.addTodo(id, { foo: 'bar' })
// throws Error: missingTitle

Cool!! Did you see what we did? Just by telling serverless-cqrs about our actions, reducer, and the storage adapter we want to use, it generated a write model for us. The write model lets us run commands on existing or new domain objects. Each time you run a command here’s what happens:

  • The write model takes the id param and uses the adapter to load all events for that domain object.
  • It uses the reducer to calculate the current state of the object and then passes the reduced state to the corresponding action for validation.
  • If the action is allowed, it will return a new event (or several events). The write model will use the adapter to append the new event(s) to the domain object.
  • The command then returns true to let the invoker know that it succeeded (otherwise it would have thrown an error).

Here’s an embedded environment. Try it out!

ReadModel

For the object that’s responsible for reads, we’re going to call it our ‘read model’. As described by CQRS above, this is a completely separate and independent object to our write model. The read model has no access to the write model and visa-versa.

We call the read model when we want to know the current state of a domain object. We can ask it for objects (in our example, todo’s) by passing it a single ID or many ID’s or maybe even some search parameters.

The read model has access to events generated by the write model. This does not mean it has access to the write model. Let me say that again: the read model has zero access to the write model (and visa-versa). Rather it’s able to subscribe to, or query, events from the write model’s underlying datastore.

In the simplest scenario, when you ask the read model for the todo with id 123, it loads all events for that domain object, runs them through its own reducer, and returns the result to you. This is important: the read model’s reducer may be the same write model’s reducer, but it doesn’t have to be. Maybe the write model likes dates as milliseconds and the read model likes the them as ISO-8601 strings, no problem-o! Maybe the write model doesn’t actually care who completed the todo, just that it’s been completed, but the read model wants to show the user who completed it. That’s fine too. They each have different reducers and get to interpret events in a way that makes sense to themselves without having to worry each other.

Let’s try this out:

//load the write model builder
const { readModelBuilder } = require('serverless-cqrs')
//load an in-memory adapter builder
const memoryAdapterBuilder = require('serverless-cqrs.memory-adapter')
// build the adapter
const adapter = memoryAdapterBuilder.build({
entityName: 'todo'
})
// load our reducer from before
const reducer = require('./reducer')
// load the adapter from our write model so we can query events
const eventAdapter = require('./writeModel/adapter)
// and now here's the magic!
const readModel = readModelBuilder.build({
reducer,
adapter,
eventAdapter,
})
// return the '123' todo list which has two todos
const results = readModel.getById({ id: '123' })
console.log(results)

This is very similar to how we initialized the write model. We don’t pass the read model any actions because it never creates events. Instead we pass it the adapter from the write model so that it can query or subscribe to events.

This brings us to another topic: how do events flow from the write model to the read model? There are basically two options, push or pull.

Pull: Whenever a query is made, the read model needs to first ask the write model adapter if any new events occured. If they have, the read model needs to apply those events before processing the query and returning results. This adds a bit of overhead to each query but simplifies the overall process.

Push: If the write model adapter supports subscriptions, the read model can ask to be notified whenever a new event is added. It can then proactively update itself as changes are made and new events are added.

Which method you prefer depends a lot on your use case. You may even choose to use a combination of both (as I do). More about this later, for now let’s look at an example:

We’re doing two things different in this example.

  1. Since we’re using an in-memory adapter and these REPLs are isolated environments, we don’t have access to the events we previously added, so we manually insert them again.
  2. We are using the ‘pull’ method by calling refresh() before running the query.

This concludes part 2 of the series. In part 3, we’ll learn how deploy this app to AWS Lambda using the Serverless framework.

Thanks for reading! ❤️

--

--