Better domain modeling using Event Sourcing

By Mattias Holmqvist - October 11, 2017

The implementation of our core domain model should be the neatest part of our code base. We like to make an extra effort to make it as readable as possible and we also tend to be stricter when it comes to keeping immutability.

However, developing a neat intention-revealing model with no side-effects and a clear business language can be tricky.

We want to use value objects instead of primitive data structures as much as possible to create a rich model that speaks the language of the domain. We also want to keep the aggregate root itself immutable to make it easy to reason about and test.

In this article we’ll see how using Event Sourcing can help us implement an immutable domain model.

You can find the full sample code for this article here:

https://bitbucket.org/serialized-io/samples-java

Note: this is a Java example but it should be easy to port to any language.

An Event Sourced Aggregate Root

A few things might look a bit different than usual when building an immutable, event-sourced domain model. First of all, we regard our methods to our Aggregate Root Order as commands, which could either fail or return one or more events:

public class Order {

  private final OrderStatus status;
  private final Amount orderAmount;

  public Order(OrderStatus status, Amount orderAmount) {
    this.status = status;
    this.orderAmount = orderAmount;
  }

  public OrderPlacedEvent place(CustomerId customerId, 
                                Amount amount) {
    status.assertNotYetPlaced(); // Will fail if wrong status
    return orderPlaced(customerId, orderAmount); 
  }
  public OrderPaidEvent pay(Amount paidAmount) {
    status.assertPlaced();
    Amount amountLeft = orderAmount.clearAmount(paidAmount);
    return orderPaid(paidAmount, amountLeft);
  }
}

What’s interesting here is that we encapsulate the domain logic in value objects (Amount and OrderStatus) but we don’t save the new state in the aggregate root in these methods. The aggregate root can be thought of as a number of functions that take value objects as arguments, execute the business logic and return events.

Important: This means we never have any getters or expose the aggregate root’s internal state in any way!

The event class is responsible for extracting the data from the value objects that need to be saved.

public class OrderPlacedEvent extends OrderEvent {

  public Data data = new Data();

  public static OrderPlacedEvent orderPlaced(CustomerId customerId,                 
                                             Amount orderAmount) {
    OrderPlacedEvent orderPlacedEvent = new OrderPlacedEvent();
    orderPlacedEvent.data.orderAmount = orderAmount.amount;
    orderPlacedEvent.data.customerId = customerId.id;
    return orderPlacedEvent;
  }

  ...
  ...
  ...
}

Once the event is created, it can be saved to an event store and used to materialize the state of the aggregate root when it is loaded.

Let’s have a look at how to load an aggregate root using events!

Separate state loading from business logic

We looked at how the business logic is being executed in our aggregate root, using value objects and keeping the state internal to the aggregate root.

To load our aggregate root from our event store we load the events using the Serialized API, then use a Builder to take the stream of events and create an immutable state object that consists of all value objects that we need to execute the business logic on our Order.

public class OrderState {

  public final String orderId;
  public final Integer version;
  public final OrderStatus orderStatus;
  public final Amount orderAmount;

  private OrderState(Builder builder) {
    orderId = builder.orderId;
    version = builder.version;
    orderStatus = builder.orderStatus;
    orderAmount = builder.orderAmount;
  }

  public static OrderState loadFromEvents(String orderId, 
                                          Integer version, 
                                          List<OrderEvent> events) {
    return OrderState.builder(orderId, version)
                     .apply(events)
                     .build();
  }

  public static Builder builder(String orderId, Integer version) {
    return new Builder(orderId, version);
  }

  public static class Builder {
    private final String orderId;
    private final Integer version;
    private OrderStatus orderStatus = OrderStatus.NEW;
    private Amount orderAmount;

    public Builder(String orderId, Integer version) {
      this.orderId = orderId;
      this.version = version;
    }
 
    public Builder apply(List<OrderEvent> events) {
      events.forEach(event -> event.apply(this));
      return this;
    }

    public Builder apply(OrderPlacedEvent event) {
      this.orderStatus = OrderStatus.PLACED;
      this.orderAmount = new Amount(event.data.orderAmount);
      return this;
    }

    ...
    ...
    public OrderState build() {
      return new OrderState(this);
    }
  }
}

The builder creates the state of the order given a number of events. This state is immutable which makes it easy to reason about.

We also need to do some trickery to make it possible to have a generic way to apply events to the state object when loading them from the event store. To make this work we add a method to the event classes to make them double-dispatch the materialization of the event to the state back to the builder. Now the correct method on the builder will be invoked, without losing type information.

Below is an extract of an event class using this pattern:

public class OrderPlacedEvent extends OrderEvent {

  public Data data = new Data();

  ...
  ...
  @Override
  public void apply(OrderState.Builder builder) {
    builder.apply(this);
  }

}

Putting it all together

To illustrate how all this fits together we can have a look at how a REST command resource class could look to pay a placed order (using plain JAX-RS):

@POST
@Path("pay-order")
public Response payOrder(@NotNull @Valid PayOrderCommand command) {
  OrderId orderId = command.orderId;
  OrderState state = orderEventService.load(orderId);
  Order order = new Order(state.orderStatus, state.orderAmount);
  OrderPaidEvent orderPaid = order.pay(command.amount);
  orderEventService.saveEvent(orderPaid);
  return Response.ok().build();
}

For more examples you can checkout the tests in our samples repository.

Tell us what you think! How do you implement your domain model?