Domain Entities

2024 10 01 head

Domain-driven design is a tremendous good practice for modern agile software architecture [1, 2, 3].

The approach is to model the digital solution around the business domain. The digital solutions are modular systems that can evolve over time.

The approach is used to design modular monoliths or complex microservices architectures. The digital modular monolith can evolve to a full-fledged microservices architecture if the market requires the associated scalability and resilience.

The framework enables parallel development with multiple agile teams. Domain-driven design approach encourages the clear definition of minimal interfaces between bounded domains.

Continuous integration, delivery, and deployment validate delivered increments [4]. The teams have the guaranty the integrated solution is working as expected.

DevOps culture shortens work in progress WIP [5] [6] [7]. The teams can deliver new features and bug fixes in a timely manner. This approach is a key concept in Kanban and Scrum methodologies.

But how do you model the domain entities in your digital solution?

We present an approach implemented in our open-source library Open Source Components.

Domain Entities

2024 10 01 bounded domain

A key concept in the approach is the concept of entities. An entity is a representation of a key object in the domain.

It is defined by its identity, continuity, and persistence over time rather than its attributes.

It encapsulates the state of that object through its attributes, including the aggregation of other entities. It defines any operations that might be performed on the entity. Operations should include the validation and business logic of the entity.

What is the identity of an instance in your system?

We identified three regular concepts of identities in digital solutions [1] [3].

Internal Identity

An instance is identified through an internal object identifier OID. The application is the owner of this internal identifier. An internal identifier should stay internal and not be sent to other systems.
An example is the company internal collaborator identifier.

External Identity

An instance is identified through an external object identifier ID. The application or another external system is the owner of the external identifier for a type of entities.
An example is the social security number of a collaborator.
External identifiers are natural ones and part of the domain and should always be preferred.

Name

An instance can at least partially be identified through a human-friendly name. The application or another external system is the owner of the name for a type of entities.
An example is the full name of a collaborator.

Our open-source library provides a mixin for each of these identification concepts. The below interfaces are for immutable access to the identity information. Identifiers should be set at construction time and should not be modified.

A mutable version of the mixins is provided for the cases where the property can be modified. The mutability is regularly encountered for names, seldom for identifiers and almost never for internal identifiers.

package net.tangly.core;

interface HasOid {
    long oid();
}

interface HasId {
    String id();
}

interface HasName {
    String name();
}

Entities should only have external identifiers in an ideal bounded domain. External identifiers are natural ones and part of the domain.

Internal object identifiers are a technical solution for incomplete domain entities. Incomplete often means that the digitalization of your bounded domain is hindered.

For example, in Switzerland it was long prohibited to use the social security number as an identifier for natural persons. The federal government changed the laws in 2022. But you still cannot access the social number of children. The identifier is defined but is not accessible.

Therefore, you are forced to create an internal identifier to uniquely identify natural persons in Switzerland. Similar difficulties exist in Europe to access the European natural person identifier [1].

Entities often have temporal information associated with them. This information describes the continuity of an instance.

We identified two major temporal attributes of entities:

Creation Date

The date when an entity was created.
An example is the creation date of an invoice.

Temporal Scope

The temporal range when an entity is active. The start date or the end date of the scope can be optional.
An example is the start and end of a collaborator work contract An active work contract has a start date but no end date. Another interesting temporal range is the validity range of currencies. The introduction of the Euro was the end of quite a few currencies.

Our open-source library provides a mixin for each of these temporal concepts. The below interfaces are for read-only access to the identity information. Similar interfaces are provided for read-write access.

package net.tangly.core;

interface HasDate {
    LocalDate date();
}

interface HasDateRange {
    DateRange range();                           (1)
    LocalDate from();
    LocalDate to();
}
1 The mixin defines the temporal scope of an entity. The start date is mandatory, the end date is optional. A null end date defines an open time interval.

Users often want to access human-readable information to better understand an instance [2]. They also want to classify groups of entities.

Textual Description

A textual description of the entity to provide context and user-related information.

Comments

Comments are user-specific notes describing various aspects of an entity. Comments are unstructured. The software does not generally process them.

Codes

Codes define a customer-specific enumeration type. The values are configurable and expandable [2]. The signification of a code type is hard-coded in the software. The set of values for a code type is customer-specific.

Tags

Tags define a customer-specific ontology to classify entities in their system. Tags without value are equivalent to labels. Tags with optional or mandatory values enrich the classification information with instance-specific parameter values.

The key difference between codes and tags is that you can define values for a code, but you cannot add a new code type. You can create new tags at any time.

We support Asciidoc syntax for all textual elements. The feature hugely improves the legibility and attractiveness of textual description for the users.

Our open-source library provides a mixin for each of these textual concepts. The below interfaces are for mutable access to the textual information. Similar interfaces are provided for immutable access.

package net.tangly.core;

interface HasMutableText {
    String text();
    void text(String text);
}

interface HasMutableComments {
    List<Comment> comments();
    void add(@NotNull Comment comment);
    void addComments(@NotNull Iterable<Comment> comments);
    void remove(@NotNull Comment comment);
}

interface HasMutableTags {
    Set<Tag> tags;
    boolean add(@NotNull Tag tag);
    void addTags(@NotNull Iterable<Tag> tags);
    boolean remove(@NotNull Tag tag);
    void removeAllTags();
}

public interface Code {
    int id();
    String code();
    boolean enabled();
}

The entity abstraction wraps the above mixins and introduces the concept of validating an instance.

package net.tangly.core;

interface Entity extends HasOid, HasId, HasName, HasText, HasTimeInterval, HasTags, HasComments {     (1)
    boolean validate();
}

interface MutableEntity extends Entity, HasMutableName, HasMutableText, HasMutableTimeInterval,      (2)
    HasMutableTags, HasMutableComments {
}
1 The entity interface defines the basic properties of a value entity. A concrete implementation of an entity can be a Java record.
2 The mutable entity interface extends the entity interface with the mutable properties of a value entity.

An excerpt of the provided mixins is displayed below. Documentation for the core library is available Core Library.

core-classes

User Interface

Your user wants to see and manipulate domain entities.

The entities of a specific type shall be displayed in a grid. The relevant properties are displayed in associated columns.

Your user needs to select the entities he is interested in. We provide filtering and ordering for the displayed columns. The filter conditions are additive.

A form is provided to display the details of an instance. The same form is provided for editing purposes.

View mode as readonly access to the entity information.

Edit mode as a mutable mode to the entity information. Modification includes edit, create, duplicate, and delete.

In seldom cases, it is necessary to refine the modes of the application. We have encountered a small number of systems requiring finer control.

List

The user can only view the grid and properties displayed in the grid. All other modes have access to the grid and the form representation for the selected item in the grid.

View

The user can only display the entity data.

Edit

The user can modify existing data but cannot add or delete new instances.

Create

The user can add new entities but cannot edit existing ones. Creation is either the definition of all properties for a new object or the edition for the values of a duplicate one.

Delete

The user can delete existing entities but cannot modify them or create new ones.

The defined modes support these special requirements.

The three most used modes are list, view, and edit.

The domain entity user interface article discusses the implementation of the CRUD approach in more detail.

Persistence

Persistence is based on Eclipse Store. The library provides a non-intrusive and efficient way to store domain entities in a file system or a database. The code is very straightforward to understand.

Export and import functions are provided through TSV and JSON format transformation. We use our Gleam library. You can find more information under Gleam.

The import and export functions are also used to provide a backup and restore function for the domain entities. If desired, these functions can be extended to provide a versioning and migration system for the domain entities.

Lessons Learnt

Modern Java has extensive support for shallow immutability with the record construct. We had to provide two hierarchies of mixin interfaces. One for immutable entities and one for mutable entities.

The compactness of the code necessary to create a complete CRUD user interface is very high. We found out writing such code is more efficient than using available model-driven approaches. Therefore, we decided to ditch frameworks such as openXava or Apache Causeway. [3].

The gains for using these frameworks are too small to justify the learning curve and implicit limitations of such libraries.

The ideas behind these frameworks and also promoted through domain-driven design are still worth learning how to apply them in your products.

Simple things should be simple and complex things should be possible.

The programmatic approach of our library is flexible. Developers can easily extend the provided interfaces to add new features or use their preferred libraries. You can create your specific user interface components and use them in the views.

The same applies to the persistence layer.

References

[1] E. Evans, Domain-driven design. Addison-Wesley, 2004 [Online]. Available: https://www.amazon.com/dp/0321125215

[2] V. Vernon, Domain-Driven Design Distilled. Addison-Wesley Professional, 2016 [Online]. Available: https://www.amazon.com/dp/B01JJSGE5S/

[3] V. Vernon, Implementing Domain driven Design. Addison-Wesley Professional, 2012 [Online]. Available: https://www.amazon.com/dp/B00BCLEBN8

[4] J. Humble and D. Farley, Continuous Delivery. Addison-Wesley Professional, 2010 [Online]. Available: https://www.amazon.com/dp/0321601912


1. Naturally, the Swiss social number is not compatible with the European one to spice up your modeling problems. An example of a well-defined external identifier is the plate number of a car. Another well-defined external identifier is the bank account number IBAN. This identifier is worldwide valid and supported by more and more countries.
2. Values for a code can be defined in a JSON file or in an SQL database.
3. We strongly prefer a plain coding approach over annotation-based solutions. This is certainly a major reason why we do not use these frameworks.