Command/Query responsibility separation and event sourcing are two great patterns for structuring microservices. They are a natural fit for architectures based around Kafka and streams, and help us achieve immutability and replaybility for our data. However, it's not always clear *how* you should create a CQRS-based application, and where do all the moving parts fit. In this talk, I will show how we can implement CQRS and ES in a purely functional manner. We'll see how doing this is nothing more than activating the data types and type classes already written for us: we'll use traversable functors and the State and Writer monads to elegantly construct the components that we need. We'll also touch the pitfalls here and how to integrate these components with a streaming library such as FS2.
2. About me
โข Freelance software developer
โข Doing functional programming, microservices, DevOps
โข Co-organize the Underscore Scala meetup
โข Hit me up if you want to work together!
Itamar Ravid - @itrvd 2
3. Agenda
โข Why Event Sourcing and CQRS are worth your time
โข How we can use functions to implement them
โข How abstractions from FP help us
โข (shortly) Hooking this up to a stream
Itamar Ravid - @itrvd 3
28. CQRS: Command/Query Responsibility
Separation
We store the events in an append only stream.
We handle reads using a specialized store, derived from the
events.
Now you know CQRS!
Itamar Ravid - @itrvd 28
29. What do we gain?
โข Faster response time
โข Availability
โข Determinism, take your debugging home
โข
!
Event-sourced services are like pure functions!
Itamar Ravid - @itrvd 29
30. And speaking of pure functions
Let's see some code!
The examples ahead assume:
import cats._, cats.data._, cats.implicits._
Itamar Ravid - @itrvd 30
31. Zoom in
We'll now focus on the reservation service.
Let's event-storm this!
What commands/events are we going to use?
Itamar Ravid - @itrvd 31
32. Commands
We might be asked to:
โข Create a reservation
โข Add a person to a reservation
โข Cancel a reservation
Itamar Ravid - @itrvd 32
33. Events
And these will cause the:
โข Reservation to be created
โข Reservation to be updated
โข Reservation to be canceled
Itamar Ravid - @itrvd 33
34. Command De๏ฌnitions
I promised some code, right?
sealed trait ReservationCommand
case class Create(roomId: String, guests: Int)
extends ReservationCommand
case class ModifyGuests(reservationId: String, amount: Int)
extends ReservationCommand
case class Cancel(reservationId: String)
extends ReservationCommand
Itamar Ravid - @itrvd 34
35. Event De๏ฌnitions
And we have the corresponding events:
sealed trait ReservationEvent
case class Created(id: String, roomId: String, guests: Int)
extends ReservationEvent
case class Updated(id: String, roomId: String, guests: Int)
extends ReservationEvent
case class Canceled(id: String, roomId: String, guests: Int)
extends ReservationEvent
Itamar Ravid - @itrvd 35
42. Breakdown
So technically, we need:
1. Validation of commands
2. State representation, state mutation
3. Event generation
4. Persistence
Itamar Ravid - @itrvd 42
45. Validation
So we want a function, right?
def processCommand(command: ReservationCommand): ???
Itamar Ravid - @itrvd 45
46. Validation
So we want a function, right?
def processCommand(command: ReservationCommand): Either[Error, Unit]
Itamar Ravid - @itrvd 46
47. Validation
So we want a function, right?
type Error = String
type Result[A] = Either[Error, A]
def processCommand(command: ReservationCommand): Result[Unit]
Itamar Ravid - @itrvd 47
48. Validation
Let's start with simple validation: the number of guests should be
positive.
def validateGuests(command: Create): Result[Unit] =
if (command.guests <= 0) Left("Non-positive guests")
else Right(())
Itamar Ravid - @itrvd 48
50. Stateful validation
Next, we want a stateful validation - no duplicate reservations.
To do that, we need some sort of state:
case class Reservation(id: String)
trait Reservations {
def byRoomId(id: String): Option[Reservation]
}
Itamar Ravid - @itrvd 50
51. Validation
And here's our validation:
def validateDup(reservations: Reservations,
command: Create): Result[Unit] =
reservations.byRoomId(command.roomId) match {
case Some(_) => Left("Duplicate reservation")
case None => Right(())
}
Itamar Ravid - @itrvd 51
52. Validation - continued
Our command processing now looks like this:
def processCommand(reservations: Reservations,
command: ReservationCommand): Result[Unit] =
command match {
case create: Create =>
(validateGuests(create), validateDup(create, reservations))
.tupled.void
// case modifyGuests, case cancelReservation, etc.
}
Itamar Ravid - @itrvd 52
53. Validation - continued
What's that (a, b).tupled function?
This will combine the result of both validations into a single
value:
val guestValidation: Result[Unit] = validateGuests(create)
val dupValidation: Result[Unit] = validateDup(create, reservations)
val tupled: Result[(Unit, Unit)] = (guestValidation, dupValidation).tupled
Itamar Ravid - @itrvd 53
54. Validation - continued
What's that (a, b).tupled function?
This will combine the result of both validations into a single
value:
val guestValidation: Result[Unit] = validateGuests(create)
val dupValidation: Result[Unit] = validateDup(create, reservations)
val tupled: Result[(Unit, Unit)] = (guestValidation, dupValidation).tupled
If one of them fails - both fails too.
Itamar Ravid - @itrvd 54
56. Validation - continued
What else can we say about Result?
It's an Applicative - so we can use tupled.
Itamar Ravid - @itrvd 56
57. Validation - continued
What else can we say about Result?
It's also a Monad, so we can use for comprehensions:
for {
a <- validate1(cmd)
b <- validate2(cmd, a)
} yield f(a, b)
Itamar Ravid - @itrvd 57
58. Validation - continued
A few side notes:
โข Our validation unfortunately short circuits. We don't see all
errors.
Check out cats.data.Validated for a solution.
โข Applicative s are essential for joining independent
computations. De๏ฌnitely read up on them!
Check out the mapN, *>, <* combinators.
Itamar Ravid - @itrvd 58
59. Validation - summary
This is all I'm going to say about validation!
โข Write an Either returning function
โข Give it what it needs to compute
โข Combine with the Applicative combinators
โข Pro๏ฌt Validated!
Itamar Ravid - @itrvd 59
61. Generating events
The path of least resistance is another function for generating:
def events(reservations: Reservations,
command: ReservationCommand): List[ReservationEvent]
Itamar Ravid - @itrvd 61
62. Generating events
We return the events inside Result:
def processCommand(reservations: Reservations,
command: ReservationCommand)
: Result[List[ReservationEvent]] =
command match {
case create: Create =>
(validateGuests(create), validateDup(create, reservations))
.tupled
.map(_ => events(reservations, commands))
}
Since we're using List, we can output 0, 1 or many events.
Itamar Ravid - @itrvd 62
63. Generating events
This is great if we can separate validation and event generation.
Itamar Ravid - @itrvd 63
64. Generating events
This is great if we can separate validation and event generation.
This isn't always possible!
Itamar Ravid - @itrvd 64
65. Generating events
This is great if we can separate validation and event generation.
This isn't always possible!
We'll upgrade the two functions with event generation.
Itamar Ravid - @itrvd 65
67. Generating events
There's a cool data type called WriterT we can use here:
case class WriterT[F[_], Log, Value](run: F[(Log, Value)])
Itamar Ravid - @itrvd 67
68. Generating events
We'll modify the validation return type to be:
WriterT[Result, List[ReservationEvent], A]
Which wraps values of type
Result[(List[ReservationEvent], A)]
Itamar Ravid - @itrvd 68
69. Generating events
Now, when we use the .tupled syntax, we get back:
val writer: WriterT[Result, List[ReservationEvent], Unit] =
(validateDup(reservations, command),
validateGuests(command)).tupled.void
And it actually concatenated the lists for us!
Itamar Ravid - @itrvd 69
70. Generating events
We can "peel" this using .run:
val writer: WriterT[Result, List[ReservationEvent], Unit] =
(validateDup(reservations, command),
validateGuests(command)).tupled.void
val result: Result[(List[ReservationEvent], Unit)] =
writer.run
Itamar Ravid - @itrvd 70
71. Generating events
Or, if we only want the log:
val result: Result[List[ReservationEvent]] =
writer.written
Itamar Ravid - @itrvd 71
72. Generating events
So let's give this new type a name:
type EventsAnd[A] = WriterT[Result, List[ReservationEvent], A]
Itamar Ravid - @itrvd 72
73. Actually writing those functions
Here's an example of how one of those functions would look like:
def validateGuests(command: Create): EventsAnd[Unit] =
if (command.guests <= 0)
WriterT.liftF("Non-positive guests".asLeft[Unit])
else
WriterT.putT(().asRight[Error])(List(ReservationCreated(command.id)))
Itamar Ravid - @itrvd 73
74. Generating events - summary
We're done! Our functions now validate and generate events:
def validateGuests(command: Create): EventsAnd[Unit]
def validateDup(reservations: Reservations,
command: Create): EventsAnd[Unit]
val result = (validateGuests(c), validateDup(s, c))
.tupled.void
Itamar Ravid - @itrvd 74
75. Generating events - summary
We're done! Our functions now validate and generate events:
def validateGuests(command: Create): EventsAnd[Unit]
def validateDup(reservations: Reservations,
command: Create): EventsAnd[Unit]
val result = (validateGuests(c), validateDup(s, c))
.tupled.void
Of course, if validation fails - everything still fails.
Itamar Ravid - @itrvd 75
76. A few side notes on WriterT
โข WriterT is also a Monad and an Applicative.
โข Lots of combinators - check out the docs!
โข WriterT can be quite allocation-heavy.
We'll touch a possible solution towards the end.
Itamar Ravid - @itrvd 76
78. State mutation
Our function doesn't do anything currently:
def processCommand(reservations: Reservations,
command: ReservationCommand): EventsAnd[Unit]
Itamar Ravid - @itrvd 78
79. State mutation
It's pretty awesome that we can reason about it just by looking at
the type:
(Reservations, Command) => WriterT[Result, List[Event], Unit]
Nothing else happens. WYSIWYG.
Itamar Ravid - @itrvd 79
80. State mutation
The path of least resistance is to return a new state:
def processCommand(reservations: Reservations,
command: ReservationCommand): (Reservations, EventsAnd[Unit])
Itamar Ravid - @itrvd 80
81. State mutation
The path of least resistance is to return a new state:
def processCommand(reservations: Reservations,
command: ReservationCommand): (Reservations, EventsAnd[Unit])
We're not saying anything about whether it was modi๏ฌed.
Itamar Ravid - @itrvd 81
82. State mutation
However, we want to guarantee that nothing happens if
validation fails.
def processCommand(reservations: Reservations,
command: ReservationCommand): EventsAnd[Reservations]
If validation is ๏ฌne, we get a new state.
If it fails, we keep the previous state.
Itamar Ravid - @itrvd 82
83. State mutation
But - how do we reconcile the two resulting states?
def validateGuests(s: Reservations, c: Create): EventsAnd[Reservations]
def validateDup(s: Reservations, c: Create): EventsAnd[Reservations]
val result: EventsAnd[(Reservations, Reservations)] =
(validateGuests(s, c),
validateDup(s, c)).tupled
Itamar Ravid - @itrvd 83
87. State mutation
Functional programming saves the day again- this is StateT:
case class StateT[F[_], State, Value](run: State => F[(State, Value)])
Itamar Ravid - @itrvd 87
88. State mutation
We add yet another type alias on top:
type CommandProcessor[A] = StateT[EventsAnd, Reservations, A]
def validateGuests(c: Create): CommandProcessor[Unit]
def validateDup(c: Create): CommandProcessor[Unit]
Itamar Ravid - @itrvd 88
89. State mutation
We can now compose our command processors:
def validateGuests(c: Create): CommandProcessor[Unit]
def validateDup(c: Create): CommandProcessor[Unit]
val resultProcessor: CommandProcessor[Unit] =
(validateGuests(c),
validateDup(c)).tupled.void
But where did our initial state disappear to?
Itamar Ravid - @itrvd 89
90. State mutation
We haven't run anything yet! Let's run the processor:
val result: EventsAnd[(Reservations, Unit)] = resultProcessor.run(state)
This does a lot of stuff!
Itamar Ravid - @itrvd 90
91. State mutation
Here's how we actually write one of these functions:
def validateGuests(command: Create): CommandProcessor[Unit] =
if (command.guests <= 0)
StateT.liftF(WriterT.liftF("Non-positive guests".asLeft[Unit]))
else
for {
state <- StateT.get[EventsAnd, Reservations]
events = generateEventsSomehow(state)
_ <- StateT.liftF(WriterT.tell[Result, List[ReservationEvent]](events))
newState = generateNewStateSomehow(reservations, event)
_ <- StateT.set[EventsAnd, Reservations](newState)
} yield ()
Itamar Ravid - @itrvd 91
92. State mutation
Here's how we actually write one of
these functions:
def validateGuests(command: Create): CommandProcessor[Unit] =
if (command.guests <= 0)
StateT.liftF(WriterT.liftF("Non-positive guests".asLeft[Unit]))
else
for {
state <- StateT.get[EventsAnd, Reservations]
events = generateEventsSomehow(state)
_ <- StateT.liftF(WriterT.tell[Result, List[ReservationEvent]](events))
newState = generateNewStateSomehow(reservations, event)
_ <- StateT.set[EventsAnd, Reservations](newState)
} yield ()
Itamar Ravid - @itrvd 92
93. Summary
โข I bet you're loving the boilerplate by now ;-)
It gets worse as you add more layers!
โข Don't worry though! This is solvable.
โข As before, StateT is also a Monad and an Applicative.
Itamar Ravid - @itrvd 93
94. Recap
We now have a pretty powerful type as a building block:
type Result[A] = Either[Error, A]
type EventsAnd[A] = WriterT[Result, List[ReservationEvent], A]
type CommandProcessor[A] = StateT[EventsAnd, Reservations, A]
Itamar Ravid - @itrvd 94
95. Recap
If we expand everything, we get:
type CommandProcessor[A] =
Reservations => Either[Error, (List[Event], (Reservations, A))]
Itamar Ravid - @itrvd 95
100. Is that it?
type Result[A] = Either[Error, A]
type EventsAnd[A] = WriterT[Result, List[ReservationEvent], A]
type CommandProcessor[A] = StateT[EventsAnd, Reservations, A]
The command processor can return values, signal failures, log
events, and accumulate state.
Itamar Ravid - @itrvd 100
101. Is that it?
type Result[A] = Either[Error, A]
type EventsAnd[A] = WriterT[Result, List[ReservationEvent], A]
type CommandProcessor[A] = StateT[EventsAnd, Reservations, A]
The command processor can return values, signal failures, log
events, and accumulate state.
No magic! These are just functions!
Itamar Ravid - @itrvd 101
102. Fitting this into an application
We still have some work to do:
โข we need to hook this up to a source of commands,
โข and we actually need to persist the state and events.
Itamar Ravid - @itrvd 102
103. Fitting this into an application
Where can commands come from?
Treating these as in๏ฌnite streams makes our life easier.
Itamar Ravid - @itrvd 103
104. Streams
We'll use fs2 for our examples.
Akka Streams can de๏ฌnitely work too! Check out the slides
when I publish them.
Itamar Ravid - @itrvd 104
105. fs2
fs2 is based on one type:
Stream[Effect[_], Element]
A possibly in๏ฌnite stream of Element, with effect Effect.
Effect can be cats IO, scalaz IO, Monix Task, etc.
Itamar Ravid - @itrvd 105
106. fs2
fs2 is based on one type:
Stream[Effect[_], Element]
A possibly in๏ฌnite stream of Element, with effect Effect.
Itamar Ravid - @itrvd 106
107. fs2
To run our command processor, we can use mapAccumulate:
class Stream[F[_], Element] {
def mapAccumulate[State, Out](init: State)
(f: (State, Element) => (State, Out)): Stream[F, (State, Out)]
}
Itamar Ravid - @itrvd 107
108. fs2
To run our command processor, we can use mapAccumulate:
class Stream[F[_], Element] {
def mapAccumulate[State, Out](init: State)
(f: (State, Element) => (State, Out)): Stream[F, (State, Out)]
}
We'll set:
โข Element = Command, State = Reservations
โข Out = (Reservations, List[ReservationEvent])
Itamar Ravid - @itrvd 108
112. Persisting events and state
def persist(reservations: Reservations): IO[Unit]
def produce(events: List[ReservationEvent]): IO[Unit]
stream.evalMap {
case (state, events) =>
(persist(state), produce(events))
.tupled
.void
}
Itamar Ravid - @itrvd 112
113. Where to go from here
โข The solution to monad transformer overhead: ๏ฌnally tagless
โข Keep the effect abstract, interpret into Task+IORef.
โข Check out the links on next slide.
โข You can also hire me and I'll help you make it work ;-)
Itamar Ravid - @itrvd 113
114. Where to go from here
Resources:
โข Ben Stopford, Designing Event-Driven Systems:
https://www.con๏ฌuent.io/designing-event-driven-systems
โข Martin Kleppman's blog:
https://martin.kleppmann.com
โข The FS2 guide:
https://functional-streams-for-scala.github.io/fs2/
โข Finally tagless and Free:
https://softwaremill.com/free-tagless-compared-how-not-to-commit-to-monad-too-early/
Itamar Ravid - @itrvd 114
118. Digression: Folds
If you squint, our function looks like a foldLeft:
def foldLeft[S, A](init: S)
(f: (S, A ) => S ): S
def processCommand: (Reservations, ReservationCommand) => Reservations
Itamar Ravid - @itrvd 118
119. Digression: Folds
There's also an interesting variant of foldLeft when the result
has an effect:
def foldLeftM[S, A](init: S)(f: (S, A) => IO[S]): IO[S]
Itamar Ravid - @itrvd 119
120. Digression: Folds
Since EventsAnd is a Monad, we can use foldLeftM:
val commands: List[ReservationCommand]
val resultState: EventsAnd[Reservations] =
commands.foldLeftM(Reservations.empty)(processCommand)
Any failed validation will halt the processing,
and all the events would be accumulated.
Itamar Ravid - @itrvd 120
122. Digression: Folds
Another interesting type of fold is a scan:
def scanLeft[S, A](init: S)(f: (S, A) => S): List[S]
Looks exactly like a fold, but you get a list of intermediate states.
Itamar Ravid - @itrvd 122
123. Digression: Folds
Scanning commands would get us a list of intermediate
Reservations:
val commands: List[ReservationCommand]
val states = commands.scanLeft(Reservations.empty)(processCommand)
// List(reservations1, reservations2, ...)
Itamar Ravid - @itrvd 123
124. Traversals
What's the shortest way to make processCommand work
on a List[ReservationCommand]?
def processCommand(command: ReservationCommand): CommandProcessor[Unit]
Itamar Ravid - @itrvd 124
125. Traversals
Let's try mapping.
val cmds: List[ReservationCommand]
val mapped: List[CommandProcessor[Unit]] = cmds.map(processCommand)
We get back a list of functions; not very helpful.
Itamar Ravid - @itrvd 125
127. Traversals
Lucky for us, we have the sequence function:
val cmds: List[ReservationCommand]
val mapped: List[CommandProcessor[Unit]] = cmds.map(processCommand)
val sequenced: CommandProcessor[List[Unit]] = mapped.sequence
So now we get one giant processing function that runs
everything!
Itamar Ravid - @itrvd 127
128. Traversals
As the title gave away, this can be expressed as:
val traversed: CommandProcessor[List[Unit]] =
cmds.traverse(processCommand)
And we can actually run it:
traversed.run(initState) match {
case Left(_) =>
// log the error
case Right(resultState, (events, _)) =>
// persist the events and state
}
Itamar Ravid - @itrvd 128
129. Traversals
This is our Traversable typeclass:
trait Traversable[F[_]] extends Functor[F]
with Foldable[F] {
def traverse[G[_]: Applicative, A, B](fa: F[A])
(f: A => G[B]): G[F[B]]
}
Itamar Ravid - @itrvd 129
130. Traversals
If you line traverse up with map, the effectful map analogy is
clearer:
def map [A, B](fa: F[A])(f: A => B ): F[B]
def traverse[G[_], A, B](fa: F[A])(f: A => G[B]): G[F[B]]
Itamar Ravid - @itrvd 130
131. Akka Streams
With Akka Streams, we can use the scan operator to process
commands:
class Source[Element] {
def scan[State](zero: State)(f: (State, Element) => State): Source[State]
}
If we set:
โข Element = ReservationCommand
โข State = (Reservations, List[ReservationEvent])
Itamar Ravid - @itrvd 131