Domain Entities

Introduction

Most domain objects that the end-user interacts with are likely to be domain entities, such as Customer, Order, Product and so on. These are persistent objects and which are mapped to a relational database using JPA/EclipseLink ORM.

Some domain entities are really aggregates, a combination of multiple objects. A commonly cited example of this is an Order, which really consists of both a root Order entity and a collection of OrderItems. From the end-users' perspective, when they talk of "order" they almost always mean the aggregate rather than just the Order root entity.

Eric Evans' Domain Driven Design has a lot to say about aggregate roots and their responsibilities: in particular that it is the responsibility of the aggregate root to maintain the invariants of its component pieces, and that roots may only reference other roots. There’s good logic here: requiring only root-to-root relationships reduces the number of moving parts that the developer has to think about.

On the other hand, this constraint can substantially complicate matters when mapping domain layer to the persistence layer. DDD tends to de-emphasise such matters: it aims to be completely agnostic about the persistence layer, with the responsibilities for managing relationships moved (pretty much by definition) into the domain layer.

As a framework Apache Causeway is less dogmatic about such things. Generally the domain objects are mapped to a relational database and so we can lean on the referential integrity capabilities of the persistence layer to maintain referential invariants. Said another way: we don’t tend to require that only roots can maintain roots: we don’t see anything wrong in an InvoiceItem referencing an OrderItem, for example.

Nonetheless the concepts of "aggregate" and "aggregate root" are worth holding onto. You’ll probably find yourself defining a repository service (discussed in more detail below) for each aggregate root: for example Order will have a corresponding OrderRepository service. Similarly, you may also have a factory service, for example OrderFactory. However, you are less likely to have a repository service for the parts of an aggregate root: the role of retrieving OrderItems should fall to the Order root (typically by way of lazy loading of an "items" collection) rather than through an OrderItemRepository service. This isn’t a hard-n-fast rule, but it is a good rule of thumb.

@DomainObject

Domain entities are persistent domain objects, and will typically be annotated with @DomainObject(nature=ENTITY).

Their persistence is handled by the JPA/EclipseLink ORM, taking care of both lazy loading and also the persisting of modified ("dirty") objects.

As such, they will also require ORM metadata, specified using either annotations or XML. The following sections show the basics.

Entities (JPA)

If the JPA/EclipseLink object store is to be used, then the domain entities should be annotated using JPA annotations.

This section shows a simple example. See the JPA/Eclipselink object store documentation for further information on annotating domain entities.

Class definition

For the domain class itself, this will be the @jakarta.persistence.Entity annotation and probably the @jakarta.persistence.Table annotation, as well as others to define indices and queries.

import jakarta.persistence.*;

@Entity                                                         (1)
@Table(
    schema= "simple"                                            (2)
    // ...
)
@EntityListeners(CausewayEntityListener.class)                  (3)
@Named("simple.SimpleObject")                                   (4)
@DomainObject                                                   (5)
public class SimpleObject {

    @Id                                                         (6)
    @GeneratedValue(strategy = GenerationType.AUTO)             (3)
    @Column(name = "id", nullable = false)                      (7)
    private Long id;

    @Version                                                    (8)
    @Column(name = "version", nullable = false)                 (5)
    @PropertyLayout(fieldSetId = "metadata", sequence = "999")
    @Getter @Setter
    private long version;

    //...
}
1 The @Entity annotation indicates that this is an entity to EclipseLink.
2 Specifies the RDBMS database schema for this entity. It’s recommended that the schema corresponds to the module in which the entity resides. The table will default to the entity name if omitted.
3 Required boilerplate that allows Causeway to inject domain services into the entity when retrieved from the database
4 the @Named annotation defines a logical name for the concrete class; used in security and bookmarks.
5 The @DomainObject annotation identifies the domain object to Apache Causeway (not EclipseLink). It isn’t necessary to include this annotation — at least, not for entities — but it is nevertheless recommended.
6 Specified the primary key, indicating that the database will assign the key, for example using an identity column or a sequence.
7 Indicates the column name (though this would be inferred) and nullability (such a primary keys should not be nullable).
8 The @Version annotation is useful for optimistic locking; the strategy indicates what to store in the version column.

Scalar Properties

All domain entities will have some sort of mandatory key properties. Additional annotations are also required to define their scalar properties and relationships to other entities.

The example below is a very simple case, where the entity is identified by a name property. This is often used in database unique indices, and in the toString() implementation:

import jakarta.persistence.*;
import lombok.*;

@Table(
    schema= SimpleModule.SCHEMA,
    uniqueConstraints = {
        @UniqueConstraint(name = "SimpleObject__name__UNQ",
                          columnNames = {"name"})               (1)
    }
)
public class SimpleObject
             implements Comparable<SimpleObject> {              (2)

    // ...
    public SimpleObject(String name) {
        setName(name);
    }

    @Column(nullable=false, length=50)                          (3)
    @Getter @Setter                                             (4)
    @ToString.Include                                           (5)
    private String name;

    private final static Comparator<SimpleObject> comparator =
            Comparator.comparing(SimpleObject::getName);

    @Override
    public int compareTo(final SimpleObject other) {
        return comparator.compare(this, other);                 (6)
    }
}
1 EclipseLink will automatically add a unique index to the primary surrogate id (discussed above), but additional alternative keys can be defined using the @Unique annotation. In the example above, the "name" property is assumed to be unique.
2 Although not required, we strongly recommend that all entities are naturally Comparable. This then allows parent/child relationships to be defined using SortedSets; RDBMS after all are set-oriented.
3 Chances are that some of the properties of the entity will be mandatory, for example any properties that represent an alternate unique key to the entity. The @Column annotation specifies the length of the column in the RDBMS, and whether it is mandatory.

Given there is a unique index on name, we want this to be mandatory.

We can also represent this using a constructor that defines these mandatory properties. The ORM will create a no-arg constructor to allow domain entity to be rehydrated from the database at runtime (it then sets all state reflectively).

4 Use Lombok to generate the getters and setters for the name property itself.
5 Use Lombok to create a toString() implementation that includes the value of name property.
6 Use java.util.Comparator#comparing() to implement Comparable interface.

Queries

When using JPA, it’s also common for domain entities to have queries annotated on them. These are used by repository domain services to query for instances of the entity:

import jakarta.persistence.*;

@NamedQueries({
    @NamedQuery(                                  (1)
        name = "SimpleObject.findByNameLike",     (2)
        query = "SELECT so " +                    (3)
                "FROM SimpleObject so " +
                "WHERE so.name LIKE :name"
    )
})
public class SimpleObject { /* ... */ }
1 There may be several @NamedQuery annotations, nested within a @NamedQueries annotation) defines queries using JPAQL.
2 Defines the name of the query.
3 The definition of the query, using JPQL syntax.

To actually use the above definition, the framework provides the RepositoryService. This is a generic repository for any domain class.

The corresponding repository method for the above query is:

public List<SimpleObject> findByName(String name) {
    return repositoryService.allMatches(                (1)
            Query.named(SimpleObject.class,             (2)
                        "SimpleObject.findByNameLike")  (3)
                   .withParameter("name", name)         (4)
            );
}

@Inject RepositoryService repositoryService;
1 find all instances that match the query
2 Specifies the class that is annotated with @NamedQuery
3 Corresponds to the @NamedQuery#name attribute
4 Corresponds to the :name parameter in the query JPQL string