Blog Arolla

Trying to set up my mind on Redux as an event sourcing system

When starting a project using Redux, developers often wonder after which pattern they should name actions. E.g, considering a counter application: INCREMENT_COUNTER or COUNTER_INCREMENTED?

This is certainly not only a question of naming. In fact, it reveals at least 3 entangled questions:

  • Are we speaking of commands or events?
  • Should the action bear a payload?
  • Which stereotype should support the business rules?

Let’s forget Redux for a while and focus first on the broader event sourcing theory.

 

Commands and Events

The event sourcing approach models an application with two functions:

  • A decision function, which reacts to commands and emits events based on the business rules and the current application state;
  • An evolution function, which updates the application state based on events.

INCREMENT_COUNTER suggests a command (with no payload), while COUNTER_INCREMENTED suggests more an event (with payload).

One must decide whether commands or events will be the golden source. Both are useful, each supposing you are pursuing a specific goal (beyond the scope of the article). The difference arises when it comes to replay history in a situation where the business rules have changed. Let’s consider the following:

 

Current business rule

The counter is increased by 1 each time a command is received.

Command Event State
0 (initial state)
INCREMENT_COUNTER COUNTER_INCREMENTED by 1 1
INCREMENT_COUNTER COUNTER_INCREMENTED by 1 2
INCREMENT_COUNTER COUNTER_INCREMENTED by 1 3

 

Updated business rule

The counter is increased by 2 each time a command is received.

Assuming commands are the golden source, let’s replay history with commands:

Command Event State
0 (initial state)
INCREMENT_COUNTER COUNTER_INCREMENTED by 2 2
INCREMENT_COUNTER COUNTER_INCREMENTED by 2 4
INCREMENT_COUNTER COUNTER_INCREMENTED by 2 6

 

Now, assuming events are the golden source, let’s replay history with events:

Command Event State
0 (initial state)
COUNTER_INCREMENTED by 1 1
COUNTER_INCREMENTED by 1 2
COUNTER_INCREMENTED by 1 3

Replaying history with events leads to the actual final state (3), while replaying history with commands leads to a different final state (6). Both approaches are valid, none is better than the other per se. It’s a matter of context, and it’s definitely a decision that should be made by business people.

Now let’s get back to Redux.

 

How does Redux fit in this model?

Though you should always keep it (stupid) simple and avoid over engineering, it is common that you end up using at least one middleware. Let’s examine how Redux and 3 widely used middlewares fit in this model.

Reminder: thunks, epics and sagas are middlewares used for orchestration and side effects. Despite their differences, the three of them allow to dispatch synchronous or asynchronous actions and can read the current state. Hence they allow to leverage a single user action by doing complex things like XHRs and conditional dispatches based on business rules and additional information from state.

Thunks, epics and sagas are usually considered competitors (you would use one or the other), though some projects use two amongst the three simultaneously.

 

Actions only

How the basic Redux approach fits in the event sourcing model is actually a matter of choice: one can decide to dispatch actions that are commands or events.

user => INCREMENT_COUNTER => (smart) reducer
Command Decide Event Evolve
INCREMENT_COUNTER Reducer Reducer

 

user => COUNTER_INCREMENTED by 1 => (dumb) reducer
Command Decide Event Evolve
Action creator COUNTER_INCREMENTED by 1 Reducer

 

Here we refer to reducers as smart or dumb reducers because in the former case the reducer is in charge of decision and evolution, while it is in charge only of the evolution in the latter case.

Dumb reducer actually means a reducer in charge of CRUD operations (Create, Read, Update and Delete), which can be made fully generic (see redux-generic).

 

Thunks

Thunks must be invoked explicitly, meaning that there is no command. Then, thunks can dispatch other thunks or actions (events), so in the end they dispatch events:

user => incrementCounter() => COUNTER_INCREMENTED by 1 => (dumb) reducer
Command Decide Event Evolve
Thunk COUNTER_INCREMENTED by 1 Reducer

 

Epics

Contrary to thunks, epics react to actions (commands), and they dispatch actions (events):

user => INCREMENT_COUNTER => (reducer, does nothing => )epic => COUNTER_INCREMENTED by 1 => (dumb) reducer
Command Decide Event Evolve
INCREMENT_COUNTER Epic COUNTER_INCREMENTED by 1 Reducer

 

Sagas

Similarly to epics, sagas react to actions (commands) and dispatch actions (events):

user => INCREMENT_COUNTER => (reducer, does nothing => )saga => COUNTER_INCREMENTED by 1 => (dumb) reducer
Command Decide Event Evolve
INCREMENT_COUNTER Saga COUNTER_INCREMENTED by 1 Reducer

 

A room for improvements?

Epics and Sagas

As seen above, redux-observable (Epics) and redux-saga (Sagas) best fit in the event sourcing model since they work with actions that are commands or events.

However, a room for improvement would be to separate commands and events so that one can choose the golden source to replay history. Referring to both of them under the same name “actions” doesn’t make much sense : they are two different things. Besides, I would expect commands not to hit reducers, which implies a deep rewrite of Redux.

At least, for the sake of avoiding ambiguity, an action could bear a meta attribute, making it clear whether it’s a command or an event:

interface Action {
   type: string;
   meta: 'COMMAND' | 'EVENT';
}

What if neither an epic nor a saga is required? If nothing but a plain Redux action is required to react to a given user action, well, that’s too bad because you’ll have to create a command OR an event, breaking the established pattern. Hence regular actions won’t integrate smoothly with epics and sagas. And creating an epic or a saga despite the fact you actually don’t need an epic or a saga is probably a bad idea.

 

Thunks

What about redux-thunk? For greater traceability, I would find beneficial that thunks are triggered by actions, like epics and sagas. But then, the same commands / events separation concern would arise, so maybe the actual situation is for the best: only events are triggered.

What if no thunk is required? If no thunk is required to react to a given user action, simply create a plain action, or should I say an event. This will integrate well with thunks, which, in the end, always trigger events.

 

Conclusion

When starting a project using Redux, a lot of questions arise: do I need a middleware? Which one? If so, which stereotype should support the business rules? etc. Thinking in terms of event sourcing helped me in making those decisions, hopefully it will help you as well.

Those decisions are mainly for the sake of clarity, but until now everything happens on the browser side only. Should you be willing to actually benefit event sourcing, you'd simply have to log commands or events and persist them server-side.

Please share your feedback through comments!

 

Further reading

http://www.arolla.fr/blog/2014/11/event-sourcing-a-scala-io-2014/

https://github.com/reduxjs/redux/issues/891

Plus de publications

Comments are closed.