Blog

Towards an immutable domain model – monads (part 5)

This is the fifth and final part of this series. In this last part we’ll reduce the boilerplate code related to handling events and as a bonus we’ll also make handling validation a bit nicer. But before we take a deep dive into the code, let’s consider the design of the last three Invoice implementations.

Other parts

Design comparison

Since this part is mostly about reducing boilerplate from the typed implementation of invoice without significantly affecting the design of our domain classes, it makes sense to take a quick look at the design of the three event sourced implementations so far: mutable (part 2), immutable (part 3), and typed/monadic (part 4/5). Which one, if any, is the “best”? Let’s compare them based on XP’s rules of simple design.

Rule Mutable Immutable Typed / Monadic
Pass all tests ++ ++ ++
Clear, expressive, & consistent + + ++
No duplication +
Minimal methods, classes + + -/+

So all implementations pass the tests and therefore satisfy the requirements. Splitting the invoice class into three subclasses improved expressiveness and reduced duplication (no need for some runtime checks), at the cost of adding some new methods and classes.

But the main highlight is that we were able to radically redesign and reimplement our domain code with minimal impact on our clients or infrastructure. We still generate exactly the same events as in the first mutable event sourced implementation and only the calling syntax was slightly changed (which usually is contained in a controller or service layer anyway). The reporting side of the application, any views, integration with external systems, etc. are unaffected!

So by having our events as a stable abstraction we have greatly decoupled the components of our application while making each component more cohesive. This gives us confidence that as requirements change we can keep our domain clean and simple, without needing to compromise our implementation with respect to durability or reporting needs.

So now that we have this out of the way, let’s get back into the code.

Composing event handlers

Let’s take a look at the applyEvent method in the DraftInvoice class from part 4:

def applyEvent = {
  case event: InvoiceRecipientChanged => applyRecipientChanged(event)
  case event: InvoiceItemAdded => applyItemAdded(event)
  case event: InvoiceItemRemoved => applyItemRemoved(event)
  case event: InvoiceSent => applySent(event)
  case event => unhandled(event)
}

It’s basically checking the type of the event and then dispatching to the correct event handler using a case-block. Each event handler is simply a function that takes an event of the correct type and returns the appropriate response. We could try to use some reflection magic to remove the need for this method, but that feels like cheating. Let’s try to if we can compose our typed event handlers into the generic applyEvent method instead.

Fortunately, Scala has a built-in trait called PartialFunction that has the useful method orElse that does exactly what we need. We just need to turn our event handling functions into partial functions to make this work.

We do this by making the type of our handlers explicit using a new class EventHandler and adding an implicit conversion from our handler type to partial functions. The partial function checks the event’s type and invoke the handler if the type is correct:

protected class EventHandler[Event, +Result](callback: Event => Result) {
  def apply(event: Event) = // [... code omitted ...]

  def applyFromHistory(event: Event) = callback(event)
}

protected def handler[A, B](callback: A => B) = new EventHandler(callback)

implicit protected def handlerToPartialFunction[A, B](handler: EventHandler[A, B])(implicit m: Manifest[A]) =
  new PartialFunction[AnyRef, B] {
    def isDefinedAt(event: AnyRef) = m.erasure.isInstance(event)

    def apply(event: AnyRef) = handler.applyFromHistory(event.asInstanceOf[A])
  }

Here an instance of EventHandler can be created using the handler method. The EventHandler class has a applyFromHistory method which simply passes the event to the provided callback.

The handlerToPartialFunction takes an EventHandler and an implicit Manifest and turns it into a partial function that takes an event of type AnyRef (Scala’s equivalent of Java’s Object class). When the partial function is invoked it downcasts the event and delegates to the EventHandler‘s applyFromHistory method. But the partial function is only defined when the provided event’s type matches the type expected by the event handler!

Using these definitions we can now update our typed event handlers and easily compose them for use with applyEvent, using the standard PartialFunction.orElse method:

def applyEvent = applyRecipientChanged orElse applyItemAdded orElse applyItemRemoved orElse applySent

private def applyRecipientChanged = handler {event: InvoiceRecipientChanged =>
  copy(event :: uncommittedEvents, recipient_? = event.recipient.isDefined)
}

Now the applyEvent method is just a single line listing all the applicable event handlers, without all the boilerplate. The typed event handlers are slightly changed to include the call to the handler factory method.

Extracting uncommitted events

The other part of boilerplate code was related to storing and managing the list of uncommitted events. Each aggregate root subclass needed to provide an accessor for the uncommittedEvent field and implement the markCommitted method. Each event handler then had to ensure the new event was prepended to the list of uncommitted event, which is somewhat error-prone.

The first step to fixing this is to remove the list of uncommitted events from the aggregate root and to start explicitly passing it into our methods, which then returns the updated list together with the updated invoice. This will certainly clean up the invoice subclasses, such as the PaidInvoice listed below.

class PaidInvoice extends Invoice {
  def applyEvent = unhandled
}

Unfortunately it makes methods like pay and applyPaymentReceived rather ugly, and we’re not even talking yet about the client code which now needs to manually manage the list of uncommitted events:

def pay(uncommittedEvents: List[Any]) =
  applyPaymentReceived(InvoicePaymentReceived(id, new LocalDate), uncommittedEvents)

private def applyPaymentReceived(event: InvoicePaymentReceived, uncommittedEvents: List[Any]) =
  (event :: uncommittedEvents, new PaidInvoice)

Fortunately, monads can help us with that. But first we’ll make our types explicit (a recurring theme of this series).

Let’s look at the types we have now. The pay method above has the type List[Any] => (List[Any], PaidInvoice). Let’s call the type of pay a Behavior which returns a Reaction when triggered by passing it a list of events. A reaction can either be Accepted for success or Rejected when failed:

trait Reaction[+T]
case class Accepted[+T](events: List[Any], result: T) extends Reaction[T]
case class Rejected(message: String) extends Reaction[Nothing]

trait Behavior[+A] {
  protected def apply(events: List[Any]): Reaction[A]

  // [... code omitted ...]

  def reaction = apply(Nil)

  def changes = reaction.asInstanceOf[Accepted[_]].events

  def rejected = reaction.asInstanceOf[Rejected].message
}

The Behavior class defines an abstract method apply that takes the current list of uncommitted events as argument and should implement the specific behavior we want. It also adds a reaction method that invokes the behavior with the empty list. The changes and rejected methods are just there for convenience.

We’ll also define a few additional methods to create some useful behaviors:

object Behaviors {
  def behavior[T](callback: List[Any] => Reaction[T]) = new Behavior[T] {
    protected def apply(events: List[Any]) = callback(events)
  }

  def accept[T](result: T) = behavior(events => Accepted(events, result))

  def reject(message: String) = behavior(_ => Rejected(message))

  def record(event: Any) = behavior(events => Accepted(event :: events, ()))

  def guard(condition: Boolean, message: => String) = if (condition) accept() else reject(message)
}

The behavior method lets us easily create a new behavior by providing a callback, instead of having to define a new anonymous subclass implementation every time.

Accept doesn’t modify the list of uncommitted events but simple returns a specific result, reject forgets about any uncommitted events and returns an error message, record records the provided event and returns an uninteresting value, and guard rejects when the condition does not hold and returns an uninteresting value otherwise.

Now we’ll just need two more pieces to complete the puzzle. The first thing we need to be able to do is to compose two behaviors into a single new behavior. We do this by first triggering the first behavior, and if successful, passing the result from the first behavior into the second behavior. This is the monad bind operation, which is called flatMap in Scala and we make it part of our Behavior trait:

trait Behavior[+A] {
  protected def apply(events: List[Any]): Reaction[A]

  def flatMap[B](next: A => Behavior[B]) = behavior {events =>
    this(events) match {
      case Accepted(updatedEvents, result) => next(result)(updatedEvents)
      case Rejected(message) => Rejected(message)
    }
  }

  // [... code omitted ...]
}

With all of this in place, we can define the EventHandler.apply method which is used by our invoice implementation to call event handlers when not reloading from history:

protected class EventHandler[Event, +Result](callback: Event => Result) {
  def apply(event: Event) = record(event) flatMap (_ => accept(callback(event)))

  // [... code omitted ...]
}

The apply method simply records the event and then accepts whatever the result of the event handler’s callback is. The return value of record is ignored, since it is of type Unit and not interesting.

So let’s take a look at the DraftInvoice.send method of our new Invoice.scala:

def send: Behavior[SentInvoice] =
  guard(readyToSend_?, "recipient and items must be specified before sending") flatMap {_ =>
    val now = new LocalDate
    applySent(InvoiceSent(id, sentDate = now, dueDate = now.plusDays(14)))
  }

// [... code omitted ...]

private def applySent = handler {event: InvoiceSent => new SentInvoice(id, event.dueDate)}

This send method’s only changes are the return type (Behavior[SentInvoice]) and the use of the guard method to perform validation. Again flatMap is used to sequence the behavior. So if the guard fails the applySent is never performed. The implementation of applySent is now also cleaned up and no longer has to worry about recording the event as this is taken care of by EventHandler.apply.

Client code is now unfortunately a bit more complicated, since it needs to deal with the monad:

"draft invoice" should {
  val invoice: DraftInvoice = Invoice.loadFromHistory(Seq(InvoiceCreated(1)))

  "support adding invoice items" in {
    val updated = invoice.addItem("Food", "2.95") flatMap (_.addItem("Water", "1.95")) flatMap (_.removeItem(1))

    updated.changes must contain(InvoiceItemAdded(1, InvoiceItem(1, "Food", "2.95"), "2.95"))
    updated.changes must contain(InvoiceItemAdded(1, InvoiceItem(2, "Water", "1.95"), "4.90"))
    updated.changes must contain(InvoiceItemRemoved(1, InvoiceItem(1, "Food", "2.95"), "1.95"))
  }

  "not be ready to send" in {
    invoice.send.rejected must beEqualTo("recipient and items must be specified before sending")
  }
}

Here the need to sequence the different behaviors using flatMap is ugly. Fortunately, this can be improved by using a more convenient name (bind or >>=) and in many cases, a service will only invoke a single method on an aggregate at a time, so the need for sequencing behaviors may be rare. Validation checking has improved, since there is no longer a need to catch exceptions, improving readability and making it easier to combine multiple validation results into one.

Conclusion

This series of blog posts has taken us through a whirlwind tour of modeling a simple invoice example. Starting out with a straightforward JPA implemention we quickly moved to event sourcing that made it possible to implement an immutable domain model. This allowed us to raise the level of abstraction of our code, increase clarity, and increase type safety. Finally we used some functional programming techniques to reduce boilerplate. Even with the additional classes and event handling methods, the monadic version of Invoice is only slightly larger than the mutable event sourced invoice. The test code is significantly smaller, since it no longer needs to test for various run-time checks, as the compiler takes care of that.

But the most significant advantage of event sourcing is the ability to change the implementation of the domain in radical ways, making it possible to keep up with changing business requirements and allowing us to keep improving the domain as our knowledge and understanding improves. All this with very little impact on the rest of the system and no need to perform database migrations.

Furthermore, our reporting and querying needs are also decoupled from the domain. Now we can easily use an RDBMS, document store, lucene index, graph database, and/or anything else that is most appropriate for our specific querying and reporting needs, without any impact on our domain model.

Finally, critical business data has become much more durable, as we only add events, and never update or delete. Full historical data is maintained, which potentially holds a great amount of business value. We now also have access to full audit logs for regulatory or debugging purposes, etc. This is incredibly valuable!

The design space for CQRS, event sourcing, and immutable domain models is still wide open. It will be very interesting to see how this evolves, and when and where these techniques are applicable. Certainly it makes it possible to apply standard functional programming techniques to “inherently” mutable business domains, with all the goodness that entails.

Zilverline gebruikt cookies om content en advertenties te personaliseren en om ons websiteverkeer te analyseren. Ook delen we informatie over uw gebruik van onze site met onze partners voor adverteren en analyse. Deze partners kunnen deze gegevens combineren met andere informatie die u aan ze heeft verstrekt of die ze hebben verzameld op basis van uw gebruik van hun services.

Okee