Blog

Towards an immutable domain model – immutable change (part 2)

In the first part of this series an Invoice domain object was defined as a starting point for discussing immutable domain objects. JPA and Scala were used for the example implementation. In this part we’ll look at this example from a different perspective to move closer to an immutable domain model.

Other parts

“Inherent mutability”

One of the first misconceptions that needs addressing is that an invoice is somehow “inherently mutable”. This may true in the “real world” (where an invoice might be a piece of paper that anyone just writes on), but when it comes to the models we use to build software systems, there is really no need to let the real world limit our designs.

The first step is to make the implicit notion of “change-over-time” (mutability) explicit in our model. The trick is to model all state changes of the invoice explicitly as an event. This event becomes our explicit notion of change. Using events to model all state changes is also known as event sourcing, journaling or transaction logging. Notice that this is nothing new: accountancy has been doing this for many centuries!

For our invoice example we can define the following events:

case class InvoiceItem(id: Int, description: String, amount: BigDecimal)

sealed trait InvoiceEvent {
  val invoiceId: Int
}

case class InvoiceCreated(invoiceId: Int) extends InvoiceEvent
case class InvoiceRecipientChanged(invoiceId: Int, recipient: Option[String]) extends InvoiceEvent
case class InvoiceItemAdded(invoiceId: Int, item: InvoiceItem, totalAmount: BigDecimal) extends InvoiceEvent
case class InvoiceItemRemoved(invoiceId: Int, item: InvoiceItem, totalAmount: BigDecimal) extends InvoiceEvent
case class InvoiceSent(invoiceId: Int, sentDate: LocalDate, dueDate: LocalDate) extends InvoiceEvent
case class InvoiceReminderSent(invoiceId: Int, reminderDate: LocalDate) extends InvoiceEvent
case class InvoicePaymentReceived(invoiceId: Int, paymentDate: LocalDate) extends InvoiceEvent

We can then use these events to describe any valid invoice, for example:

  1. Invoice 17 created
  2. Invoice 17’s recipient changed to “Erik”
  3. Item added to invoice 17 with description “Beverage” and amount 2.95
  4. Invoice 17 sent on 2011-2-1 with payment due date 2011-2-15
  5. Payment received for invoice 17 on 2011-2-13

Notice that these events are all named using the past tense. The events represent the results of behaviors that have already happened, not the behaviors themselves. By defining these events we have simultaneously reduced the number of possible mutations and made it easier for business domain experts to understand the system. We’ve raised the level of abstraction.

Also notice that the use of events to capture important facts about the business domain is a perfect match for the durability need. Since events are immutable and never deleted, you can be much more certain that no important information is ever lost, unlike our JPA example which overwrites the reminder date whenever a new reminder is sent, or where bugs could easily lead to corruption of supposedly durable data.

See this presentation by Greg Young for much more business related benefits.

Event sourced invoice implementation

So let’s move on to the implementation of an invoice using event sourcing. To do this we need to make two major changes to our implementation:

  1. Change the behavior to generate and track events instead of mutating state directly
  2. Restore the state of the Invoice from its historical events.

Lets capture this in the following trait:

trait AggregateRoot[Event] {
  protected def applyEvent: Event => Unit

  def uncommittedEvents: Iterable[Event] = _uncommittedEvents

  def markCommitted = _uncommittedEvents.clear

  def loadFromHistory(history: Iterable[Event]) = history.foreach(applyEvent)

  protected def record(event: Event) {
    applyEvent(event)
    _uncommittedEvents += event
  }

  private val _uncommittedEvents = mutable.Queue[Event]()
}

The trait is parameterized (generic) over the type of events the aggregate root can handle. The first declared method (applyEvent) is abstract. This method is used to update the current state according to the given event and returns no value of interest (Unit, the Scala equivalent of void).

The next three methods (uncommittedEvents, markCommitted, and loadFromHistory) allow the clients of our aggregate root to load from and persist to a durable store. The uncommittedEvents are stored in a mutable collection while the loadFromHistory method simply applies (plays back) each event in the history to the current instance.

Finally the record method allows our Invoice implementation to update its current state and record this change in the collection of uncommittedEvents.

An example usage (using Scala Specs syntax) can be found below. Here we first load an invoice’s history, invoke some behavior, and check that correct event was generated:

"ready to send invoice" should {
  "generate invoice sent event" in {
    val invoice = new Invoice
    invoice.loadFromHistory(Seq(
      InvoiceCreated(1),
      InvoiceRecipientChanged(1, Some("Erik")),
      InvoiceItemAdded(1, InvoiceItem(1, "Food", 2.95), 2.95)))

    invoice.send

    invoice.uncommittedEvents must contain(
      InvoiceSent(1,
        sentDate = new LocalDate(2011, 1, 29),
        dueDate = new LocalDate(2011, 2, 12)))
  }
}

The full implementation of the event sourced Invoice can be found at Invoice.scala. Let’s take a look at the send method:

def send {
  require(!sent_?, "invoice already sent")
  require(readyToSend_?, "recipient and items must be specified before sending")
  val now = new LocalDate
  record(InvoiceSent(id, sentDate = now, dueDate = now.plusDays(14)))
}

The method first checks the current state to see if the invoice is ready to be send. If so, it calls the record method (defined in the AggregateRoot trait) with a new InvoiceSent event. The record method will store the event in the uncommittedEvents collection and invoke the Invoice’s applyEvent method, of which an excerpt is listed here:

protected def applyEvent = {
  // [... code omitted ...]
  case event: InvoiceSent =>
    sent_? = true
    dueDate = Some(event.dueDate)
  // [... code omitted ...]
}

One important thing to notice is that since the Invoice implementation is no longer concerned with durability or reporting needs, we only need to track the state necessary to fulfill the behavioral contract. For example, we simply use a boolean flag to remember that the invoice was sent. We don’t need the actual sent date, since no behavior currently requires it. The same goes for various other fields, such as the recipient’s name or the payment date. However, the due date is needed, as it is used to check if we can send a reminder.

So now that the domain model is freed of the durability and reporting responsibilities, it has become both a smaller and more focused implementation of the behavioral needs, compared to the original JPA implementation.

Reports and queries

I won’t be going into the details on reporting in this series, but it should be clear that it is easy to define any kind of report based on the events generated by the domain. These reports can even be created many years after the original events were generated, allowing you to define new ways to look at historical data.

Conclusion

By splitting the JPA Invoice class into three different parts, aligned by the needs for durability, reporting, and behavior, each part becomes easier to implement and is better suited to the need.

There is still mutable state, but the mutations are now isolated into the applyEvent method and each change is now an explicit, immutable event. In the next parts we’ll look at making the invoice immutable and how that can help us in the design of the domain model.

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