Exploring Event Sourcing and Related Patterns in Haskell
Haskell is an ideal language to explore event sourcing (ES) concepts.Table of Contents
Introduction
The Event Sourcing Pattern (Young 2010, 17; Fowler 2005a) is typically used in conjunction with the CQRS Pattern (Homer et al. 2014, 45) to form the “CQRS/ES” superpattern . Event Sourcing is about storing an application’s state as a sequence of events—that reproduce the state when replayed in the right order—as opposed to simply storing the “current state”. The reason as to why CQRS and ES go “hand in hand” is that CQRS suggests that updates should be modelled as commands, which are similar to events although they depict an intention rather than an actual outcome.
Introducing Event Sourcing creates new problems for which new solutions are required. For instance, events may be missing or wrong. In this case, the Retroactive Event Pattern is proposed by Fowler (2005c). Likewise, Event Sourcing also lends itself to new interesting features such as Parallel Models, also proposed by Fowler (2005b).
In this article, we will explore the Event Sourcing, Retroactive Event and Parallel Model patterns using a minimal Haskell model. In order to gain a better understanding of the problem space, a rather detailed scenario is presented based on the modelling of an airline’s miles loyality programme.
Problem
We will first provide a general business scenario as a narrative and then support it with as Haskell model to serve as the formal problem description. Later, in the Solution section, we will rexamine the model by introducing the Event Sourcing pattern.
Business Scenario
Jane is enrolled on the Super Miles programme operated by Van Damme Airlines, headquartered in Brussels. The Super Miles programme allows passengers to upgrade to business class in exchange of 10,000 miles.
Van Damme Airlines is currently running a promotion which doubles the miles of every flight following a London (LHR) > Brussels (BRU) connection in order to prompt passengers to take long haul flights from Brussels rather than from London.
Jane has just arrived to Bangkok (BKK) from London (LHR) via Brussels (BRU) for business—really, she is not headed to Khao San Road^{1}.
Knowing that the distance between Brussels and Bangkok is about 5,000
miles, Jane expects to have accumulated over 10,000 miles (with the
Super Miles promotion) so that she can return to London in business
class after so many days of partying business meetings.
Jane approaches the Van Damme Airline’s desk to request the upgrade to business class for her return leg back to London but she is told that she hasn’t got enough miles for that:
“Madam, your Super Miles account has only got 5730 miles.”
“That’s not possible”, Jane complains and then explains the conditions under which the Super Miles promotion should have given her about double the quoted mileage.
In the end, it turns out that the LHR > BRU leg was not entered onto the system, so the Van Damme’s clerk proceeds to enter this leg which awards Jane an extra 217 miles and says:
“Apologies for the mistake Madam. However, you only have 5947 miles after including the missing leg so you still haven’t got the necessary 10,000 miles required for an upgrade to business class”.
The clerk cannot enter miles directly onto the system for security reasons and he is unable to trigger the promotion retroactively. Jane is told that she would need to contact the Customer Care to look into the issue. After giving up, Jane asks one more question:
“I need to come back to Bangkok in two months time again, if I fly directly from London, would I have enough miles to fly both in and out in business class without the need to come via Brussels in order to double my miles?”
The clerk replies: “That is looking too much ahead into the future. If you don’t get the necessary miles you can always top them up by taking one more flight”. It is a calculation way too complex for the Clerk to perform and the system does not allow to enter hypothetical routes either.
Jane decides to never fly with Van Damme Airlines again.
Haskell Model
These are a few minor imports that are required by the examples to follow:
1import Data.List(sort,sortBy,nubBy)
2import Data.Time.Clock.POSIX(getPOSIXTime)
3import Control.DeepSeq
We first define a few types to describe our problem domain which includes airports, miles, routes and a special type of route called “last route” which may double the points of a following route:
1data Airport = LHR  BKK  BRU deriving (Show,Eq,Ord,Enum,Bounded)
2type Route = (Airport,Airport)
3type LastRoute = Route
4type Miles = Int
Van Damme Airlines does not allow clerks to enter miles manually, so we need an internal function that translates routes to miles:
1distance :: Route > Miles
2distance (LHR,BRU) = 217
3distance (LHR,BKK) = 5930
4distance (BRU,BKK) = 5730
5distance (a ,b )  a == b = 0
6  otherwise = distance (b,a)
Whenever an Super Miles account is opened, its balance is zero:
1initialMilesBalance :: Miles
2initialMilesBalance = 0
We now assume that the clerk’s inability to solve Jane’s problems was
due to the system’s way of handling updates. The assumption is that
there is a general updateMiles
function which takes an optional last
route, the current route, the current state of the miles account, and
that returns the new mileage count based on the mentioned inputs:
1updateMiles :: Maybe LastRoute > Route > Miles > Miles
2updateMiles lastRoute route miles = miles+multi*(distance route)
3 where multi = case lastRoute of
4 Just (LHR,BRU) > 2
5 _ > 1
The above is an update function combined with the business rule. In the
miles+multi*(distance route)
formula, multi
is always 1
(the exact
number of miles is added) except in the case in which the last route is
(LHR,BRU)
.
With the described types and functions we can now describe what happened to Jane’s miles balance in more precise terms:
First, Jane opens a Super Miles account:
1> let balance = initialMilesBalance
2> balance
30
Then, Jane flies from London (LHR) to Brussels (BRU), and from there, to Bangkok (BKK), however the first leg is not accounted, only the second one:
1> let balance' = updateMiles Nothing (BRU,BKK) balance
2> balance'
35730
When the clerk was alerted of the missing leg (LHR,BRU)
, he had no
other option than to add it without specifying a last route which is
Nothing
:
1> let balance'' = updateMiles Nothing (LHR,BRU) balance'
2> balance''
35947
Had the clerk specified (LHR,BRU)
as the last route, he would have
also specified a current route, resulting in extra miles being added.
For example:
1> let bad_balance = updateMiles (Just (LHR,BRU)) (BRU,BKK) balance'
2> bad_balance
317190
This is because, the (BRU,BKK)
would be counted twice, once without
the doubling promotion and once with it:
1> bad_balance == distance (BRU,BKK) + 2*distance (BRU,BKK)
2True
What should have happened here is that we should have calculated the
last entered distance as though the (LHR,BRU)
had taken place before:
1> let good_balance = updateMiles (Just (LHR,BRU)) (BRU,BKK)
2 . updateMiles Nothing (LHR,BRU)
3 $ initialMilesBalance
4> good_balance
511677
The clerk is also unable to use the system to determine the implication of potential routes without updating the system itself when Jane asks about the best plan to gain sufficient miles to obtain her business class upgrade—this problem in particular is less obvious in our immutable functional model.
Summary of Problems
What we see here is two problems:

The inability to correct the present state based on an event that should have happened in the past—a missed leg in a flight.

The diffuculty in predicting a future state (number of available miles) based on a sequence of potential events from the current state onwards.
Solution
The key idea in the Event Sourcing pattern is that the state is stored
as a series of events. For this purpose, we declare an Event
type:
1data Event = Enrol
2  Departure Airport
3  Arrival Airport
4  MilesReward Reward
5  CloseAccount
6 deriving (Show,Eq,Ord)
The combination of Departure
and Arrival
events award miles whereas
a MilesReward
event subtracts miles from the balance. For now, let’s
assume a Business Class upgrade reward identified by the BizClass
value and Luxury Meal reward identified by the LuxuryMeal
value. The
latter is just for illustration and will not be used throughout the
discussion.
1data Reward = BizClass  LuxuryMeal
2 deriving (Show, Eq, Ord)
For simplicity, we are abstracting away from multiple accounts (we assume the model applies only to Jane’s account) and also from time other than “before” and “after”.
We will now proceed to implement the Create, Delete and Update operations. “Read” is actually the most complex one in event sourcing so this will be a separate discussion.
Creating an account implies generating a first initial Enrol
event:
1create :: [Event]
2create = [Enrol]
Deleting an account is a matter of adding a CloseAccount
event:
1delete :: [Event] > [Event]
2delete events = CloseAccount:events
Updating the account involves adding events for departures, arrivals and rewards:
1departure :: Airport > [Event] > [Event]
2departure airport events = (Departure airport):events
1arrival :: Airport > [Event] > [Event]
2arrival airport events = (Arrival airport):events
1reward :: Reward > [Event] > [Event]
2reward reward events = (MilesReward reward):events
For example, the composition of the following functions characterises the opening of a Super Miles account, a flight from London to Brussels, and the cancellation of the account:
1> delete . arrival BRU . departure LHR $ create
2[CloseAccount,Arrival BRU,Departure LHR,Enrol]
So far we have a neat sequence of events, but we do not have the balance of remaining miles which we did in the simpler model presented in the Problem section. This is what will solve in the next section.
Computing State
So far we have a list of events [Event]
with departures and arrivals
and what we want is the number of miles that have been
accumulated—involved potential promotions such as the doubling of
miles for any flight following a LHR > BRU route.
This means that whenever we are process an event Event
, we are always
asking three questions:

What is the total number of miles accumulated so far?

What was the last departure airport?

Do we have to double the miles for the current leg?
Therefore, we define a function called updateState
that takes a single
event, a tuple which holds the answer to the above questions and that
then returns another updated tuple.
1updateState :: Event  Current Event
2 > (Miles,Maybe Airport,Int)  Current State
3 > (Miles,Maybe Airport,Int)  New State
The tuple (Miles,Maybe Airport,Int)
works as follows:

Miles
holds the current miles balance, 
Maybe Airport
tells what was the last departure airport (if any, otherwise it isNothing
), 
Int
type that holds the multiplier to be applied to current leg. Normally it is1
but it will be2
(in order to double the miles) if the last leg was LHR > BRU.
We will now proceed to implement every “use case” when interpreting an event.
Use Case 1: Departure from any airport.
Whenever a departure occurs, there is no change to the balance and neither to the multiplier but we need to save what the last departure airport was using the tuple’s second element:
1updateState (Departure from) (balance,_,multi) =
2 (balance,Just from,multi)
For example:
1> updateState (Departure LHR) (0,Nothing,1)
2(0,Just LHR,1)
Use Case 2: Promotional LHR > BRU route detected.
In this case, the event is an arrival to BRU
when the last departure
airport was LHR
. This use case has a two fold effect: it updates the
balance with the distance between LHR
and BRU
using the current
multiplier but it also sets the multiplier to 2
so that miles can be
doubled in the next leg.
1updateState (Arrival BRU) (balance,Just LHR,multi) =
2 (balance + multi * distance (LHR,BRU),Just LHR,2)
For example:
1> updateState (Arrival BRU) (0,Just LHR,1)
2(217,Just LHR,2)
Use Case 3: Arbitrary route detected.
This is the detection of an Arrival
event whenever a prior departure
airport exists. In this case, the number of miles is calculated taking
the Airport
value from the Arrival
event and that from the second
element of the tuple. The multiplier is applied to the distance and then
it is set to 1
in order to reset it to the default in case it was a
different number before which would be the case if the LHR > BRU leg
was detected before.
1updateState (Arrival arrival) (balance,Just departure,multi) =
2 (balance + multi * distance (arrival,departure),Just departure,1)
For example:
1> updateState (Arrival BKK) (0,Just BRU,1)
2(5730,Just BRU,1)
3> updateState (Arrival BKK) (0,Just BRU,2)
4(11460,Just BRU,1)
Use Case 4: Business Class upgrade applied.
This involves subtracting 10,000 miles from the account. To avoid overcomplicating the model, we do not check whether there is sufficient balance to discount the 10,000 miles.
1updateState (MilesReward BizClass) (balance,lastDeparture,multi) =
2 (balance  10000,lastDeparture,multi)
For example:
1> updateState (MilesReward BizClass) (11460,Nothing,1)
2(1460,Nothing,1)
Use Case 5: Any other event
Here we simply ignore the event and pass the state unchanged.
1updateState _ (balance,lastDeparture,multi) =
2 (balance,lastDeparture,multi)
For example:
1> updateState (MilesReward LuxuryMeal) (11460,Nothing,1)
2(11460,Nothing,1)
The updateState
only takes one Event
value at a time, so we will
apply it using foldr
to an event list. This will result in applying
updateState
to every single state whilst also passing the state from
one application to the next.
1foldState :: [Event] > (Miles,Maybe Airport,Int)
2foldState = foldr updateState (0,Nothing,1)
For instance, performing the route LHR > BRU results in the accumulation of 217 miles and the setting of the multiplier to 2 for the next trip:
1> foldState [Arrival BRU,Departure LHR,Enrol]
2(217,Just LHR,2)
Finally, the “read” function which returns the account balance is
implemented by the countMiles
function which simple takes the first
coordinate from the tuple returned by foldState
:
1countMiles :: [Event] > Int
2countMiles events =
3 (\(miles,_,_) > miles) $ foldState events
For example:
1> countMiles [Arrival BRU,Departure LHR,Enrol]
2217
Reporting
It is also useful to obtain a “statement”, similar to a bank one so that
we can see how each event affects the miles balance. First we create a
general statement
function which takes a list of events and returns a
new list consisting of the same events but relating each one with the
balance at the time of the event.
1statement :: [Event] > [(Event,Int)]
2statement (x:xs) = (x,countMiles (x:xs)):statement xs
3statement [] = []
For example:
1> statement [Arrival BRU,Departure LHR,Enrol]
2[(Arrival BRU,217),(Departure LHR,0),(Enrol,0)]
Then we define a pretty printer function called report
:
1report :: [Event] > IO ()
2report events = do
3 putStr $ format ("Event","Miles Balance")
4 putStrLn $ replicate 37 ''
5 putStr $ concatMap (\(a,b) > format (show a,show b))
6 (statement events)
7 where format (a,b) = concat [take 22 (a ++ repeat ' ')
8 ," ",b,"\n"]
For example:
1> report [Arrival BRU,Departure LHR,Enrol]
1Event  Miles Balance
2
3Arrival BRU  217
4Departure LHR  0
5Enrol  0
Retroactive Event Pattern
We will now look at Jane’s problem using the new model based on Event Sourcing considering the Retroactive Event Pattern.
Fowler (2005c) describes three kinds of retroactive events:

Outoforder events: those that have been received late and are therefore in the “wrong order”. This is indeed the case in our example when clerk has added the LHR > BRU route when Jane was already in Bangkok.

Rejected events: events that should not have been processed. For example, awarding Jane a Business Class upgrade when her account’s balance was zero.

Incorrect events: probably valid event types but that carry invalid information. For example, recording the arrival airport as
LHR
rather thanBKK
.
Furthermore, Fowler (2005c) mentions that dealing with retroactive events can be thought as three parallel models, one which represents the “incorrect reality” (the current model) such as that pertaining the Jane’s case, a “correct branch”, which represents the model had the events taken place as expected, and finally, the “corrected reality” which is simply the elevation of the “correct branch” model to an official, current status.
Let’s start with the incorrect reality:
1> let incorrect = [Arrival BKK, Departure BRU,Enrol]
2> report incorrect
1Event  Miles Balance
2
3Arrival BKK  5730
4Departure BRU  0
5Enrol  0
When the clerk adds the LHR > BRU leg late, we also have a second incorrect reality:
1> let incorrect2 = Arrival BRU:Departure LHR:incorrect
2> report incorrect2
1Event  Miles Balance
2
3Arrival BRU  5947
4Departure LHR  5730
5Arrival BKK  5730
6Departure BRU  0
7Enrol  0
As we can see, adding the LHR > BRU leg late does not double the miles of the BRU > BKK leg. So what we need to do here is place said events in the correct order. Here, the Retroactive Event pattern introduces a new problem: the insertion of an event between other two events.
In our Haskell model, the order of events, that is, what events occur first and what events occur later, is determined by their location in a regular Haskell list. There are multiple ways in which a Haskell list may be split and recombined to place outoforder events in the right place. Even though Haskell lists are not naturally indexed, since they are recursive types rather than arrays, we will treat the list of elements as indexed because we need to be able to select an event range and the location in which the event range will be moved to:
1moveEvents :: (Int,Int) > Int > [a] > [a]
2moveEvents (start,end) to events =
3 map snd
4 . sortBy (\(x,_) (y,_) > compare x y)
5 . map (\(n,e) > (if n >= start && n <= end then to*m+n else n*m,e))
6 $ zip
7 [1..] events where m = 10^(ceiling $ logBase 10 (fromIntegral end))
We now show the incorrect event sequence again, fix it, and then show
the corrected version by using the moveEvents
function above:
1> report incorrect2
1Event  Miles Balance
2
3Arrival BRU  5947
4Departure LHR  5730
5Arrival BKK  5730
6Departure BRU  0
7Enrol  0
1> let corrected = moveEvents (1,2) 4 incorrect2
2> report corrected
1Event  Miles Balance
2
3Arrival BKK  11677
4Departure BRU  217
5Arrival BRU  217
6Departure LHR  0
7Enrol  0
We can now appreciate the Miles Balance is correct since the BRU > LHR leg has resulted in the doubling of the miles applicable to the BRU > BKK leg.
The Issue in Inserting Retroactive Events
As we had said before, the placing of one or more events between two events is not trivial. We will discuss here some of the most approachable, but not necessarily most efficient, solutions:
 Rebuild the event sequence from scratch
 Use the event’s time stamp
 Use a custom ordering property
 Use a secondary “changes” event stream
Rebuild The Event Sequence from Scratch
Unless we have a relatively small number of events—in the thousands rather than in the millions—this approach will be rarely practical. However, there is also the issue of having to identify events which requires some sort of temporary indexing property which uniquely identifies each event.
To illustrate this issue, we will see how the moveEvent
function works
from a conceptual perspective:
Step One: Assign an integer to each event so that it is possible to select the event range and the insertion point:
Therefore, moveEvents (1,2) 4 events
translates to:
Int  Event  Selection  Insertion 

1  Arrival BRU  start = 1  
2  Departure LHR  end = 2  
3  Arrival BKK  
4  Departure BRU  after 4  
5  Enrol 
Step Two: Increment the temporary ordering property to make room for the moved events—this is not trivial since we may not leave a sufficient gap to move all the events, we will explain this issue in the next section.
Int  Event 

10  Arrival BRU 
20  Departure LHR 
30  Arrival BKK 
40  Departure BRU 
50  Enrol 
Step Three: Change the selected events' ordering property to a number below insertion point. Here we sum the old events' indices with the new incremented insertion point’s index. So, 40 + 1 becomes 41 and 40 + 2 becomes 42:
Int  Event 

41  Arrival BRU 
42  Departure LHR 
30  Arrival BKK 
40  Departure BRU 
50  Enrol 
Step Four: Reorder by the ordering property in ascending order:
Int  Event 

30  Arrival BKK 
40  Departure BRU 
41  Arrival BRU 
42  Departure LHR 
50  Enrol 
Step Five: Drop the ordering property so that we end up simply with the new sequence of events.
We see that in the case of rebuilding an event sequence, we will use an
ordering property somehow even if it is temporarily. The actual
algorithmic implementation can simply rely on counters and be “tail
recursive” in spirit—it is not necessary to create a data structure of
type [(Int,a)]
. However, it is nevertheless an expensive computation
specially in a production system in which events are likely to be
persisted to disk.
Use The Event’s Timestamp
In this case, we assume the following:
 That events have a timestamp property.
 That the timestamp property is usermodifiable.
 That the order of events is determined by the timestamp property.
First, we need to understand what happens when the command interface receives two events at exactly the same time. Would they be written with the same timestamp?
A collection of events with the same timestamp would effectively constitute a set rather than a sequence so it would not be possible to tell which event comes “before” or “after”. Alternatively, the command interface could always increment the timestamp by one unit when two events occur at once. This approach, though, creates new problems:

The “order” imposed by the writer is arbitrary and may be undesirable; in other words, it may be useful to tell if two events have actually occurred “at once”.

Depending of the timestamp resolution, too many events occurring at the same time could result in many events being artificially set to a time excessively distant into the future. Say for instance that the timestamp resolution is at the second level. If 65 events are received at once, five of them will have a timestamp as though they had taken place one minute later. This would be catastrophic in a lowlatency trading system in finance, for instance.
Thus, we see that in general, altering the timestamp property, if possible at all, comes with non minor tradeoffs.
Use a Custom Ordering Property
Here we either have a userdefined property that determines the event’s order or a predefined argument that is part of the event sourcing interface. This approach is straightforward on the surface; for example, we can simply establish that the greater number is always last and sort in descending order:
1> sortBy (\x y > compare (fst y) (fst x))
2[(2,'b'),(1,'c'),(3,'a')]
3[(3,'a'),(2,'b'),(1,'c')]
The problem here is that in order to insert an event “in the middle”,
say, element x
between a
and b
, we need a gap. Some of possible
solutions are:
1) “Make room” Solution
This solution involves reindexing existing events in order to “make room” for the new event to be inserted. For example:
Step  List  Comments 

1  [(3,'a'),(2,'b'),(1,'c')] 
Original list 
2  [(4,'a'),(2,'b'),(1,'c')] 
Change (3,'a') to (4,'a') 
3  [(4,'a'),(3,'x'),(2,'b'),(1,'c')] 
Add (3,'x') 
2) “Reindexing” Solution
This involves incrementing all the indices by a factor that allows sufficient space to place the events between two indexed elements. This is the approach we’ve taken in our Haskell model.
Step  List  Comments 

1  [(3,'a'),(2,'b'),(1,'c')] 
Original list 
2  [(30,'a'),(20,'b'),(10,'c')] 
Multiply by 10 
3  [(30,'a'),(22,'x'),(20,'b'),(10,'c')] 
Add (20+2,'x') 
3) “Big steps” Solution
This solution requires events to be indexed with numbers that have a step larger or equal than two (for instance 10) so that events can be inserted in between. For example:
Step  List  Comments 

1  [(30,'a'),(20,'b'),(10,'c')] 
Original list 
2  [(30,'a'),(25,'x'),(20,'b'),(10,'c')] 
Add (25,'x') 
4) “Rational Numbers” Solution
If we were to use rational numbers, there is always room to place an extra number between two other numbers. This requires the ordering property to have two components. Native floating point types may be used—if their precision is well understood—instead.
Step  List  Comments 

1  [((3,1),'a'),((2,1),'b'),((1,1),'c')] 
Original list 
2  [((3,1),'a'),((5,2),'x'),((2,1),'b'),((1,1),'c')] 
5/2 == 2.5 
The indices for the above list would be equivalent to 3.0, 2.5, 2.0 and 1.0.
The discussed solutions are from the point of view of trying to implement retroactive events on top of a conventional store such as a SQL RDBMS. When operating only inside of a programming language’s environment, algorithmically more efficient solutions such as double linked lists may be helpful.
Use a Secondary “Changes” Event Stream
If the event store is “append only” as it is often referred as (Young 2010, 32), then it may not actually be possible to modify the events that have been already been written. In this case, an alternative solution is to have a secondary stream of events that represent “overrides” to the primary one.
In order to do this, we need to index the events so that we can uniquely identify them:
1incorrectIndexed :: [(Int,Event)]
2incorrectIndexed = zip [1..]
3 [Arrival BRU
4 ,Departure LHR
5 ,Arrival BKK
6 ,Departure BRU
7 ,Enrol]
Then we need to define a stream of “change” events that overrides the original stream:
1data Change = DROP
2  INSERT Event
1override :: [(Int,Change)]
2override = [(1,DROP)
3 ,(2,DROP)
4 ,(5,INSERT (Arrival BRU))
5 ,(5,INSERT (Departure LHR))
6 ]
Finally, we need to produce a new corrected stream which takes into account both the incorrect stream and the stream of changes:
1readEvents :: [(Int,Event)] > [(Int,Change)] > [Event]
2readEvents events changes = f events changes
3 where f [] _ = []
4 f ((i,e):es) [] = e:(f es [])
5 f ((i,e):es) ((ic,c):cs)
6  i == ic = case c of
7 DROP > f es cs
8 INSERT ev > ev:(f ((i,e):es) cs)
9  otherwise = e:(f es ((ic,c):cs))
Here we arrive to the same result as that obtained using the
moveEvents
function but using a different approach:
1> report $ readEvents incorrectIndexed override
1Event  Miles Balance
2
3Arrival BKK  11677
4Departure BRU  217
5Arrival BRU  217
6Departure LHR  0
7Enrol  0
The readEvents
function has a overly simplistic implementation since
it expects all changes to a given indexed event to be in the same order.
If we wanted to do “changes of changes” we would need to maintain
multiple change streams and apply them in tandem to the incorrect branch
that they aim to fix before composing them with the next batch of
changes.
Parallel Models
We now turn our attention to the Jane’s second request:
“I need to come back to Bangkok in two months time again, if I fly directly from London, would I have enough miles to fly both in and out in business class without the need to come via Brussels in order to double my miles?”
In other words, we are trying to find out whether a hypothetical sequence of events would lead to a given outcome; in this case, whether Jane would have accumulated 10,000 miles before arriving to Bangkok, and whether she would have 10,000 miles before departing from Bangkok in her way to London.
We could iterate through all possible routes, but since we only have three airports, the possibilities aren’t too numerous.
Let’s suppose that the current state is the incorrect one and that Jane will not be credited with the miles pertaining the missed leg:
1currentState = [Arrival BKK
2 ,Departure BRU
3 ,Enrol]
In all cases, we assume that Jane would return to London, upgrade to Business Class and depart from there again:
1outbound = [Departure LHR
2 ,MilesReward BizClass
3 ,Arrival LHR
4 ,Departure BKK
5 ]
6 ++ currentState
Also, we assume that she will arrive to Bangkok and try to claim the Business Class reward again:
1inbound = [MilesReward BizClass
2 ,Arrival BKK
3 ]
From here on, we have two parallel models. One in which Jane flies directly to Bangkok and the other in which she first makes a stopover in Brussels in order to double her miles:
1direct = inbound ++ outbound
2stopover = inbound
3 ++[Departure BRU
4 ,Arrival BRU
5 ]
6 ++outbound
We now run a report on both routes and compare the results:
1> report direct
1Event  Miles Balance
2
3MilesReward BizClass  2410
4Arrival BKK  7590
5Departure LHR  1660
6MilesReward BizClass  1660
7Arrival LHR  11660
8Departure BKK  5730
9Arrival BKK  5730
10Departure BRU  0
11Enrol  0
1> report stopover
1Event  Miles Balance
2
3MilesReward BizClass  3337
4Arrival BKK  13337
5Departure BRU  1877
6Arrival BRU  1877
7Departure LHR  1660
8MilesReward BizClass  1660
9Arrival LHR  11660
10Departure BKK  5730
11Arrival BKK  5730
12Departure BRU  0
13Enrol  0
As per the above two reports, the direct route results in a negative miles balance if Jane happens to fly directly. This means she would not be able to upgrade to Business Class; instead, she requires to use the route with a change in Brussels, which triggers the miles doubling promotion.
More Examples
The business scenario example was rather prescriptive. We can also use parallel models in a more “generative” fashion. Let’s first write a function to generate the number of possible flights that may be added to a list of existing events:
1possibleRoutes :: [[Event]] > [[Event]]
2possibleRoutes events =
3 [ Arrival a:Departure d:es
4  a < [ minBound :: Airport .. maxBound :: Airport]
5 , d < [ minBound :: Airport .. maxBound :: Airport ]
6 , es < events
7 , a /= d
8 , case es of { ((Arrival a'):_) > d == a' ; _ > True }
9 ]
For example:
1> possibleRoutes [[Enrol]]
2[[Arrival LHR,Departure BKK,Enrol]
3,[Arrival LHR,Departure BRU,Enrol]
4,[Arrival BKK,Departure LHR,Enrol]
5,[Arrival BKK,Departure BRU,Enrol]
6,[Arrival BRU,Departure LHR,Enrol]
7,[Arrival BRU,Departure BKK,Enrol]
8]
If we now apply this function recursively, we will have a route generator which creates infinite parallel models. To make the function more useful, we also add the number of miles that each of the routes produces:
1findAllRoutes = find [[Enrol]] where
2 find routes = let routes' = possibleRoutes routes
3 in map (\r > (r,countMiles r)) routes'
4 ++ find routes'
Since the function will never stop, we have to limit the number of answers that we want:
1> take 7 $ findAllRoutes
2[([Arrival LHR,Departure BKK,Enrol],5930)
3,([Arrival LHR,Departure BRU,Enrol],217)
4,([Arrival BKK,Departure LHR,Enrol],5930)
5,([Arrival BKK,Departure BRU,Enrol],5730)
6,([Arrival BRU,Departure LHR,Enrol],217)
7,([Arrival BRU,Departure BKK,Enrol],5730)
8,([Arrival LHR,Departure BKK,Arrival BKK,Departure LHR,Enrol],11860)
9]
With such a power, we can now ask questions such as “Could you give 2 examples of routes that will produce at least 35,000 miles?":
1> mapM_ report
2 . take 2
3 . map (\(route,_) > route)
4 . filter (\(route,miles) > miles >= 35000)
5 $ findAllRoutes
1Event  Miles Balance
2
3Arrival LHR  35580
4Departure BKK  29650
5Arrival BKK  29650
6Departure LHR  23720
7Arrival LHR  23720
8Departure BKK  17790
9Arrival BKK  17790
10Departure LHR  11860
11Arrival LHR  11860
12Departure BKK  5930
13Arrival BKK  5930
14Departure LHR  0
15Enrol  0
16
17Event  Miles Balance
18
19Arrival LHR  35380
20Departure BKK  29450
21Arrival BKK  29450
22Departure LHR  23520
23Arrival LHR  23520
24Departure BKK  17590
25Arrival BKK  17590
26Departure LHR  11660
27Arrival LHR  11660
28Departure BKK  5730
29Arrival BKK  5730
30Departure BRU  0
31Enrol  0
As we can see, the routes are very similar other than from the initial departure airport. We can further constrain the query by demanding that at least one LHR > BRU leg is present so that the miles doubling promotion is applicable:
1> report
2 . fst
3 . head
4 . filter (\(route,miles) > miles > 35000
5 && [Departure LHR,Arrival BRU]
6 `isSubsequenceOf` route
7 )
8 $ findAllRoutes
1Event  Miles Balance
2
3Arrival LHR  35397
4Departure BKK  29467
5Arrival BKK  29467
6Departure LHR  23537
7Arrival LHR  23537
8Departure BKK  17607
9Arrival BKK  17607
10Departure BRU  6147
11Arrival BRU  6147
12Departure LHR  5930
13Arrival LHR  5930
14Departure BKK  0
15Enrol  0
There are multiple applications for the Parallel Model pattern. The takeaway message here is that the rules that apply to the processing of events should not be hard coded into procedures that persist data. For example, in the case of calculating miles, it should be possible to do it on tentative routes and not only when the passenger effectively travels.
Materialised View Pattern
According to Homer et al. (2014, 96), this pattern “helps to support efficient querying and data extraction, and improve application performance” by means of generating prepopulated views over the intended data in one or more data stores.
This pattern does not contribute to the functional business problem suggested by the scenario we have presented. The idea here is that whenever a query is complex (e.g. there are multiple projections and join operations) we may persist a view which is the result of more complex operations which we then query in a more “shallow” and efficient manner.
For example, the findAllRoutes
airport function is infinite and it
take a long time to process complex queries. Let’s suppose that we have
a function that provides the first route that achieves a given number of
miles:
1oneRouteByMiles :: Int > IO [Event]
2oneRouteByMiles miles = do
3 startTime < round `fmap` getPOSIXTime
4 let route = head
5 . filter (\(route,m) > m >= miles)
6 $ findAllRoutes
7 endTime < route `seq` (round `fmap` getPOSIXTime)
8 endTime `seq` putStr $ "Seconds: "
9 print $ startTime `subtract` endTime
10 return $ fst route
This function takes 14 seconds on a ThinkPad X220 laptop to find a route that accumulates at least 100,000 miles:
1> oneRouteByMiles 100000
2Seconds: 14
3[Arrival LHR,Departure BKK,..,Enrol]
If it is often the case that passengers want to know about routes that take a given number of miles, we can generate a socalled materialised view that we can query directly rather than incurring the computation expensive change of exploring all possible route permutations for every query.
1routesByMiles :: [(Int,[Event])]
2routesByMiles =
3 sort
4 . map (\(r,m) > (m*1000,r))
5 . nubBy (\x y > snd x == snd y)
6 . map (\(r,m) > (r,(round ((fromIntegral m) / 1000))))
7 . takeWhile (\(_,m) > m <= 100000)
8 $ findAllRoutes
The routesByMiles
function creates a materialised view as follows:
1> mapM_ ((\(m,r) > print (m,take 4 r))) $ routesByMiles
2
3(1000,[Arrival LHR,Departure BRU,Arrival BRU,Departure LHR,...])
4(2000,[Arrival LHR,Departure BRU,Arrival BRU,Departure LHR,...])
5(3000,[Arrival LHR,Departure BRU,Arrival BRU,Departure LHR,...])
6(4000,[Arrival LHR,Departure BRU,Arrival BRU,Departure LHR,...])
7(5000,[Arrival LHR,Departure BKK,Enrol])
8...
9(98000,[Arrival LHR,Departure BKK,Arrival BKK,Departure LHR,...])
10(99000,[Arrival LHR,Departure BKK,Arrival BKK,Departure LHR,...])
11(100000,[Arrival LHR,Departure BKK,Arrival BKK,Departure LHR,...])
12...
Then, the materialised view may be embedded in a query program:
1materialiasedViewQuery :: IO ()
2materialiasedViewQuery = do
3 putStrLn "Computing materialised view ..."
4 let view = routesByMiles
5 (length view) `seq` putStrLn $ "Done!"
6 loop view
7 where loop view = do
8 putStr "Enter the desired miles: "
9 miles < read `fmap` getLine
10 case dropWhile (\(m,r) > m < miles) view of
11 (r:_) > print $ snd r
12 _ > putStrLn "No route found"
13 loop view
In this way, the complex calculation is performed only once:
1> materialiasedViewQuery
1Computing materialised view ...
2Done!
3Enter the desired miles: 14350
4[Arrival LHR,Departure BKK,Arrival BKK,Departure LHR,Arrival
5LHR,Departure BRU,Arrival BRU,Departure LHR,Arrival LHR,Departure
6BRU,Arrival BRU,Departure LHR,Arrival LHR,Departure BRU,Arrival
7BRU,Departure LHR,Arrival LHR,Departure BRU,Arrival BRU,Departure
8LHR,Arrival LHR,Departure BRU,Enrol]
9Enter the desired miles:
Conclusion
The Event Sourcing Pattern is useful to model state that results from the sequential evaluation of a series of events such as in the case of financial transactions. The pattern is more adequate (and sometimes even mandatory) in situations in which current events are evaluated by considering previous ones—apart from accumulator variables—as in our miles doubling promotion example.
The construction and leverage of Parallel Models comes almost for free by the very nature of how data is modelled when applying the Event Sourcing Model. However, it is key that the evaluation of events can be performed upon arbitrary event streams.
The Retroactive Event Pattern is problematic when it comes to the mechanics of “inserting an event in the past” due to the lack of an immediate obvious solution to the problem of creating unlimited, arbitrary intermediate events. In an “append only” event sourcing engine, this would not even be possible and a “compensating event” strategy would be required instead.
Finally, we’ve seen that the Materialised View pattern addresses mainly a performance concern and it is fairly trivial to implement.
References
Fowler, Martin. 2005a. “Event Sourcing.” December 12, 2005. http://martinfowler.com/eaaDev/EventSourcing.html.
Fowler, Martin. 2005b. “Parallel Model.” December 12, 2005 http://martinfowler.com/eaaDev/ParallelModel.html.
Fowler, Martin. 2005c. “Retroactive Event.” December 12, 2005. http://martinfowler.com/eaaDev/RetroactiveEvent.html.
Homer, Alex, John Sharp, Larry Brader, Masashi Narumoto, and Trent Swanson. 2014. Cloud Design Patterns: Prescriptive Architecture Guidance for Cloud Applications. Microsoft. https://msdn.microsoft.com/enus/library/dn568099.aspx?f=255.
Young, Greg. 2010. “CQRS Documents.” November 2010. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf.