trait StateReader[F[_], S] {
  def read: F[S]
trait EventWriter[F[_], E] {
  def write(event: E, other: E*): F[Unit]
trait Entity[F[_], S, E] extends StateReader[F, S] with EventWriter[F, E]

Entity is parametrized with entity state S and events E. It is a typeclass which represents reader-writer capabilities for F with event-sourcing semantics, i.e. the abilities to read current entity state from the context and persist events. Entity is used to describe entity behavior (e.g. BookingEntity).

Functional event sourcing

Reader-writer is a natural fit for describing event sourcing behavior: the monadic chain represents event sequencing and corresponding evolution of the state (see also here and here).

Advantages of this abstraction are:

  • the command is the call and the reply is simply the final resulting value in the monadic chain, there are no explicit representations
  • maximal composability since it’s just flatMap all the way down, making it easier to work with a reduced set of events
  • read always provides the up-to-date state and event folding happens transparently behind the scenes
  • pure & side-effect-free logic that is easy to test
  • the algebra interpreter does not support asynchronicity or other such effects lower in the hierarchy, which makes it impossible to accidentally introduce them in the command handling logic

When defining event-sourced entities, it is considered best practice to process commands and formulate a reply quickly as it makes the system responsive. Long-running processes should only initiate as the result of events, which also has the added benefit that they can be restored upon recovery or be driven by a projection. See Effector to find out how to describe side effects with endless.

About performance

When composing a sequence of computations which has multiple writes with interspersed reads, the state is folded before each read by the interpreter. This is necessary to provide a constant version of the state during interpretation.

This is an operation that the runtime (Pekko/Akka) will also do behind the scenes when evolving the entity state. Redundant invocations of the folding function can therefore occur with the elevated monadic abstraction.

However, in most cases this overhead is insignificant:

  • event application needs to be as fast and simple as possible as it is of course invoked repeatedly for each event during recovery
  • any possible redundant folds only happens upon reception of a command, not upon recovery
  • command handling behavior with multiple interspersed reads and writes is less frequent than the more common read-then-write pattern