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.
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:
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.
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:
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.
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.
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.