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.
“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 InvoiceEventcase class InvoiceRecipientChanged(invoiceId: Int, recipient: Option[String]) extends InvoiceEventcase class InvoiceItemAdded(invoiceId: Int, item: InvoiceItem, totalAmount: BigDecimal) extends InvoiceEventcase class InvoiceItemRemoved(invoiceId: Int, item: InvoiceItem, totalAmount: BigDecimal) extends InvoiceEventcase class InvoiceSent(invoiceId: Int, sentDate: LocalDate, dueDate: LocalDate) extends InvoiceEventcase class InvoiceReminderSent(invoiceId: Int, reminderDate: LocalDate) extends InvoiceEventcase class InvoicePaymentReceived(invoiceId: Int, paymentDate: LocalDate) extends InvoiceEventWe can then use these events to describe any valid invoice, for example:
- Invoice 17 created
- Invoice 17′s recipient changed to “Erik”
- Item added to invoice 17 with description “Beverage” and amount 2.95
- Invoice 17 sent on 2011-2-1 with payment due date 2011-2-15
- 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:
- Change the behavior to generate and track events instead of mutating state directly
- 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.
Great stuff! Scala really shines here. Looking forward to the next part(s).
-Albert
Hello, great article. This is a total revelation and really helpfull. I understand programs in only objects, even when I didn’t code, and this is a little hard to grasp for me.
But I have a question: as you are mutating the object everytime it is loaded by applying all the past events, how will the performance be for an object with a 1 year lifetime with 1000s of mutations.
First of all, on an average laptop you can easily apply 10s of thousands of events per second. Reading from the database may be a bit slower. But if that’s not fast enough, then there are a couple of options:
1a) Load the aggregate only once (for example, at application startup). This works well with immutable domain objects but requires you to have enough RAM to keep everything loaded.
1b) Cache aggregates, prioritizing aggregates with many events.
2) But the most common solution is to also store snapshots. For example, you could store a snapshot every 100 events. Whenever you load the aggregate you retrieve the latest snapshots and only apply the events that have occurred since that snapshot. This may require additional coding since you somehow have to save/restore the current state of an aggregate. So obviously you’d only do this for aggregates that tend to have many events and are frequently loaded.
Thanks again, that’s great.