Implementing entity states as separate classes
Let’s say we want to implement a service that handles the process of placing an order. This order goes through multiple stages during its lifetime. Initially it is pending, then goes to confirmed and paid. Finally it ends up as either completed or canceled.
Now going to implementation it seems obvious that we need a class named Order
. Simplest way to model the states that it goes through is having an OrderState
enum.
There are different things that we can do with an Order
in each of its states. For example, in pending state we can change any details (e.g add/remove items and their quantities). After Order is confirmed we cannot allow changes so freely anymore. We might only allow some special cases of amending the confirmed Order (e.g backoffice people may need to replace some items with alternatives if we have ran out of something listed in the original version of the Order). So changing order before it’s confirmed has very different constraints than amending it afterwards.
If we model all this in a single Order class we end up having a guard clause in all command methods:
class Order {
OrderStatus status void update(ChangeOrderRequest change) {
if (status != PENDING) {
throw new ValidationException("Cannot update order", status)
}
...
} void confirm() {
if (status != PENDING) {
...
}
...
} void amend(AmendOrderRequest amend) {
if (status != CONFIRMED) {
...
}
...
} }
There are three problems with this design:
- mutability. Even though business concept Order is mutable does not mean we should give up all the benefits of immutability when implementing it
- it mixes a lot of behavior into a single class and hides away important concepts like difference between a pending Order and a confirmed Order. We want significant business concepts to be more prominent in our code.
- if we try to do something we should not be doing we will only discover it at runtime when a validation exception is thrown. This is not aligned with the idea of a mistake proof design.
Immutable entity
Making Order immutable is quite simple. We just need to change our command methods so they will return the result instead of mutating the Order instance inplace. Following the idea of using nouns for builder (command) methods we will have methods like Order changed(ChangeOrderRequest)
and Order confirmed()
.
Splitting it up
There is one annoying issue when a builder method returns the same class as the original class. It is quite easy to accidentally use the wrong instance.
For example, we want to send out an event when Order is confirmed like this:
Order confirmed = order.confirmed()
//some other code in between
events.notify(new OrderConfirmed(order)) // BUG!
All these problems can be solved when we split Order up based on its states. So we will have classes like PendingOrder
, ConfirmedOrder
, CompletedOrder
etc.
The above bug can now be avoided by only accepting ConfirmedOrder
as a parameter to the OrderConfirmed
event constructor.
Resulting classes:
class PendingOrder {
PendingOrder changed(...) { ... }
ConfirmedOrder confirmed() { ... }
}class ConfirmedOrder {
ConfirmedOrder amended(...) { ... }
PaidOrder paid(...) { ... }
CancelledOrder cancelled() { ... }
}
Alternative to these factory methods is using constructors directly. However, main benefit of having factory methods is slightly better API discoverability.
Expanding to Repositories
We can expand this approach to Repositories. Instead of a single Order Repository we can have Repository classes like: ConfirmedOrders
, PendingOrders
etc (note the naming is to emphasise that we want Repositories to be collection-like interfaces). This makes sure that we cannot accidentally load an Order thinking it is in pending state while it actually is already confirmed.
If we need to query for an Order without knowing its current state e.g for tracking its status we can implement a read-only query model like OrderDetails
.
A variation of this pattern is where we keep single Order
class but have separate interfaces for each of its states and the readonly view.
In Secure by Design book Daniel Deogun, Dan Bergh Johnsson and Daniel Sawano describe similar approach as “Entity relay”.
P.S. Interested to join us? We’re hiring. Check out our open Engineering roles.