Blog

Towards an immutable domain model – introduction (part 1)

This is the first part of a (short) series of blogs on implementing a rich domain model using only immutable domain objects. This first part introduces a (simple) example domain model and provides a JPA implementation using Scala. This exampe will serve as a baseline that should be familiar to most developers who have experience with an ORM. The other parts of this series will redesign the example to use only immutable objects and will explore some of the benefits and drawbacks of doing so.

Other parts

Why immutability?

So why even try to achieve immutability in your domain classes? What makes immutable objects useful compared to mutable ones?

  • Immutable objects are often easier to use. Compare java.util.Calendar (mutable) with Joda-Time’s DateTime (immutable).
  • Immutable objects reduce the number of possible interactions (aliasing) between different parts of the program. Just consider how hard it would be to program Java if the String class was mutable and all methods provided by String mutated the receiving instance and returned void. Sharing these strings with other parts of the program could be very dangerous.
  • Immutable objects can be safely shared between multiple threads. An example would be the implementation of a shared cache: immutable objects make this quite a bit easier.
  • Implementing an immutable object is often easier, as there is less that can go wrong and the design space is “smaller”.

Furthermore the rising availability (and popularity?) of functional programming languages (such as Clojure, F#, Haskell, and Scala) make it easier to work with immutable data than imperative languages such as Java. This can have important implications on how we design business applications, so it is worth exploring making the domain model immutable, especially since a business domain model is often considered to be inherently mutable.

A not “totally trivial” example

The example is a (simplified) version of an Invoice. Invoices consist of a recipient, invoice items, and the invoice amount. The lifecycle of our simplified invoice consists of the following stages:

  1. Draft invoices start out empty and can be freely edited
  2. Once all required information is present the invoice can be send
  3. A sent invoice can receive payment. When payment is not received before the due date we can send a reminder.

The diagram to the right depicts the main interface of our Invoice class. The methods are grouped in relation to the state of the invoice.

The full JPA implementation is available as Invoice.scala on github. Test cases are also available. Below you’ll find a small excerpt from the Invoice implementation:

@OneToMany(cascade = Array(CascadeType.ALL))
@OrderBy
private var _items: List[InvoiceItem] = new ArrayList

@Basic(optional = false)
private var _totalAmount: BigDecimal = BigDecimal.ZERO

@Temporal(TemporalType.DATE)
private var _sentDate: Date = _

def sent_? = _sentDate != null

def removeItem(index: Int) {
  require(!sent_?, "items cannot be changed after invoice is sent")
  val item = _items.remove(index)
  _totalAmount = _totalAmount.subtract(item.amount)
}

Even for such a simplified example the resulting code is already quite large and feels messy. The mix of low-level data manipulation (JPA annotations, conversions between Joda-Time dates and the Java dates supported by JPA, use of null instead of Options) with the high-level behavioral code seems to be a clear violation of the Single Responsibility Principle (SRP).

This is because we are trying to meet three separate needs that are common to many business applications inside a single class:

  1. provide a record of all “important” data (durability)
  2. implement business rules and logic (behavior)
  3. answer questions about an invoice (reporting)

The JPA implementation of the example makes various compromises with regards to these three needs:

  1. The “total amount” field is denormalized to make it easier to perform queries
  2. Only the last reminder date is kept and previous reminder dates are overwritten

Besides being messy, there is also something strange about hiding the data of an Invoice behind a behavioral interface, but then fully exposing this data to the ORM, as well as any queries and reports that access this information. Furthermore “getters” are provided so the data can be used by clients. Encapsulation should make it possible to easily change the implementation without any of the user of an Invoice being aware of this, but anyone who has tried to actually do this with JPA backed objects knows it is not as easy as it should be!

Is it possible to split this object into three different ones, each with just a single responsibility? Certainly, and many existing systems already do so. The Invoice entity can be implemented as an anemic domain object, and is used to provide the required durability. The behavioral aspects are implemented by controller, manager, or service objects, and the reporting needs are provided by a DAO. This often results in procedural transaction scripts that implement the behavioral requirements by mutating the “durable” entity. Although we could redesign our Invoice this way, it only seems to lead us further from an immutable domain model.

In the next part we’ll look at a different way meet these three basic needs which does allow an immutable implementation of the domain model. The name of the game will be the addictive combination of CQRS and Event Sourcing.

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