Building Aggregates with Spring Data
Petter Holmström
Posted on April 26, 2021
In my last post we learned how to build value objects that an be persisted with JPA. Now it is time to move on to the objects that will actually contain your value objects: entities and aggregates.
JPA has its own @Entity
concept, but it is far less restrictive than the entity concept from DDD. This is both an advantage and a disadvantage. The advantage is that it is quite easy to implement entities and aggregates with JPA. The disadvantage is that is is equally easy to do things that is not allowed in DDD. This may be especially problematic if you are working with developers that have used JPA extensively before but who are new to DDD.
Whereas value objects just implemented an empty marker interface, entities and aggregate roots will need more extensive base classes. Getting your base classes right from the start is important as it will be quite difficult to change them later, especially if your domain model has grown big. To help us with this task, we are going to use Spring Data.
Spring Data provides some base classes out of the box that you can use if you like, so let's start by looking at them.
Using Persistable
, AbstractPersistable
and AbstractAggregateRoot
Spring Data provides an interface out-of-the-box called Persistable
. This interface has two methods, one for getting the ID of the entity and another for checking whether the entity is new or persisted. If an entity implements this interface, Spring Data will use it to decide whether to call persist
(new entities) or merge
(persisted entities) when saving it. You are, however, not required to implement this interface. Spring Data can also use the optimistic locking version to determine whether the entity is new or not: if there is a version, it is persisted; if there is none, it is new. You need to be aware of this when you decide how you are going to generate your entity IDs.
Spring Data also provides an abstract base class that implements the Persistable
interface: AbstractPersistable
. It is a generic class that takes the type of the ID as its single generic parameter. The ID field is annotated with @GeneratedValue
which means that a JPA implementation such as Hibernate will try to auto-generate the ID when the entity is first persisted. The class considers entities with a non-null ID as persisted and entities with null IDs as new. Finally, it overrides equals
and hashCode
so that only the class and the ID are taken into account when checking for equality. This is in line with DDD - two entities are considered the same if they have the same ID.
If you are fine with using ordinary Java types (such as Long
or UUID
) for your entity IDs and letting your JPA implementation generate them for you when the entity is first persisted, then this base class is an excellent starting point for your entities and aggregate roots. But wait, there is more.
Spring Data also provides an abstract base class called AbstractAggregateRoot
. This is a class that - you guessed it - is designed to be extended by aggregate roots. However, it does not extend AbstractPersistable
nor does it implement the Persistable
interface. Then why would you want to use this class? Well, it provides methods that allow your aggregate to register domain events that are then published once the entity is saved. This is really useful and we will return to this subject in a later post. Also, there are some benefits to not declaring the ID field in the base class and having your aggregate roots declare their own IDs. We will also return to this subject in a later post.
In practice, you want your aggregate roots to be Persistable
and so you end up implementing either the methods of AbstractAggregteRoot
or AbstractPersistable
in your own base class. Let's have a look at how to do that next.
Building Your Own Base Classes
In virtually all projects that I work on, both at work and in private, I start by creating my own base classes. Most of my domain models are build from aggregate roots and value objects; I rarely use so called local entities (entities that belong to an aggregate but are not roots).
I often start with a base class called BaseEntity
and it looks like this:
@MappedSuperclass // <1>
public abstract class BaseEntity<Id extends Serializable> extends AbstractPersistable<Id> { // <2>
@Version // <3>
private Long version;
public @NotNull Optional<Long> getVersion() {
return Optional.ofNullable(version);
}
protected void setVersion(@Nullable Long version) { // <4>
this.version = version;
}
}
- Even though the class is named
BaseEntity
, it is not a JPA@Entity
but a@MappedSuperclass
. - The
Serializable
bound comes directly fromAbstractPersistable
. - I use optimistic locking for all my entities. We will return to this later in this post.
- There are very few, if any, situations where you want to set the optimistic locking version manually. However, to be on the safe side, I provide a protected method that makes this possible. I think most Java developers with some years under their belts have experienced situations where they would really have needed to set an attribute or call a method in a super class only to find that it was private.
Once I have the BaseEntity
class in place, I move on to BaseAggregateRoot
. This is essentially a copy of Spring Data's AbstractAggregateRoot
, but it extends BaseEntity
:
@MappedSuperclass // <1>
public abstract class BaseAggregateRoot<Id extends Serializable> extends BaseEntity<Id> {
private final @Transient List<Object> domainEvents = new ArrayList<>(); // <2>
protected void registerEvent(@NotNull Object event) { // <3>
domainEvents.add(Objects.requireNonNull(event));
}
@AfterDomainEventPublication // <4>
protected void clearDomainEvents() {
this.domainEvents.clear();
}
@DomainEvents // <5>
protected Collection<Object> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
}
- This base class is also a
@MappedSuperclass
. - This list will contain all domain events we want to publish when the aggregate is saved. It is
@Transient
because we don't want to store them in the database. - When you want to publish a domain event from within your aggregate, you register it using this protected method. We will have a closer look at this later in this article.
- This is a Spring Data annotation. Spring Data will call this method after the domain events have been published.
- This is also a Spring Data annotation. Spring Data will call this method to get the domain events to publish.
Like I said, I rarely use local entities. However, when that need arises, I often create a BaseLocalEntity
class that extends BaseEntity
but does not provide any additional functionality (except, maybe a reference to the aggregate root that owns it). I will leave this as an exercise to the reader.
Optimistic Locking
We already added a @Version
field for optimistic locking to BaseEntity
but we did not yet discuss why. In Tactical Domain-DrivenDesign, the fourth guideline for aggregate design was to use optimistic locking. But why did we add the @Version
field to BaseEntity
and not to BaseAggregateRoot
? After all, isn't it the aggregate root that is responsible for maintaining the integrity of the aggregate at all times?
The answer to this question is yes, but here, the underlying persistence technology (JPA and its implementtions) is again sneaking into our domain design. Let's assume we are using Hibernate as our JPA implementation.
Hibernate does not know what an aggregate root is - it only deals with entities and embeddables. Hibernate also keeps track of which entities have actually been changed and only flushes those changes to the database. In practice this means that even though you explicitly ask Hibernate to save an entity, no changes may actually be written to the database and the optimistic version number may remain the same.
As long as you only deal with aggregate roots and value objects, this is not a problem. For Hibernate, a change to an embeddable is always a change to its owning entity and so the optimistic version of the entity - in this case the aggregate root - will be incremented as expected. However, things change as soon as you add local entities to the mix. For example:
@Entity
public class Invoice extends BaseAggregateRoot<InvoiceId> { // <1>
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<InvoiceItem> items; // <2>
// The rest of the methods and fields are omitted
}
-
Invoice
is the aggregate root and so it extends theBaseAggregateRoot
class. -
InvoiceItem
is a local entity and so it either extends theBaseEntity
class or aBaseLocalEntity
class depending on your base class hierarchy. The implementation of this class is not important so we are leaving it out, but please note the cascading options in the@OneToMany
annotation.
A local entity is owned by its aggregate root and so is persisted through cascading. However, if a change has been made only to the local entity and not to the aggregate root, saving the aggregate root will only result in the local entity being flushed to the database. In the example above, if we made a change only to an invoice item and then saved the entire invoice, the invoice version number would remain unchanged. If another user had made changes to the same item just before we saved our invoice, we would silently overwrite the other user's changes with ours.
By adding the optimistic locking version field to BaseEntity
, we protect against situations like this. Both the aggregate root and the local entities will be optimistically locked and it will not be possible to accidentally overwrite somebody else's changes.
In the next post, we are going to look at how to build repositories with Spring Data.
Posted on April 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.