Tactical domain drives design

Translated from: vaadin.com/learn/tutor…

In this article, we will learn about tactical domain-driven design. Tactical DDD is a set of design patterns and building blocks that can be used to design domain-driven systems. Even for non-domain-driven projects, you can benefit from using some tactical DDD patterns.

Tactical design is more hands-on and closer to actual code than strategic domain-driven design. Strategic design deals with abstract wholes, while tactical design deals with courses and modules. The goal of tactical design is to refine the domain model to the point where it can be translated into working code.

Design is an iterative process, so it makes sense to combine strategic and tactical design. You start with strategic design, then tactical design. The greatest domain model design Revelations and breakthroughs are likely to occur during tactical design, which in turn affects strategic design, so you need to repeat the process.

Again, the content is heavily based on Eric Evans’s Domain-Driven Design: Addressing Complexity at the Heart of Software and Vaughn Vernon’s Implementing Domain-Driven Design, which I highly recommend you read. As in the last article, I chose to explain as much as I could in my own words and inject my own thoughts, thoughts and experiences when appropriate.

With a brief introduction, it’s time to show the tactical DDD toolkit and learn what’s in it.

The value object

One of the most important concepts in tactical DDD is the value object. This is also the DDD building block I use the most in non-DDD projects, and I hope you will do the same after reading this article.

A value object is an object whose value is important. This means that two value objects with exactly the same value can be treated as the same value object and thus interchangeable. Therefore, value objects should always be made immutable. Instead of changing the state of the value object, it is replaced with a new instance. For complex value objects, consider using manufacturer or nature schemas.

Value objects are not only data containers, but can also contain business logic. The fact that value objects are also immutable makes business operations thread-safe and side-effect free. This is one of the reasons I really like value objects, and why you should try to model as many domain concepts as possible for value objects. Also, try to make value objects as small and consistent as possible – this makes them easier to maintain and reuse.

A good starting point for making value objects is to take all the single-valued properties that make business sense and wrap them as value objects. Such as:

Instead of using aBigDecimal as a monetary value, Money uses the wrapped value object BigDecimal. If you are using more than one Currency, you may also need to create a Currency value object and wrap that Money object as a BigDecimal-Currency pair.

Instead of using strings for phone numbers and E-mail addresses, use PhoneNumber and EmailAddress assignments to wrap string objects.

Using value objects like this has several advantages. First, they create context for value. You don’t need to know whether a particular string contains a phone number, email address, name, or zip code, or whether aBigDecimal is a monetary value, percentage, or something entirely different. The type itself immediately tells you what to work with.

Second, you can add all the business operations that can be performed on a particular type of value to the value object itself. For example, a Money object can contain operations for adding and subtracting sums or calculating percentages, while ensuring that the precision of the base currency BigDecimal is always correct and that all objects involved in the Money operation have the same currency.

Third, you can be sure that the value object always contains valid values. For example, you can validate the E-mail address input string in the constructor of the EmailAddress value object.

Code example

The value object in Money might look like this (the code is untested, and some method implementations have been omitted for clarity) :

Money.java

public class Money implements Serializable, Comparable<Money> { private final BigDecimal amount; private final Currency currency; // Currency is an enum or another value object public Money(BigDecimal amount, Currency currency) { this.currency = Objects.requireNonNull(currency); this.amount = Objects.requireNonNull(amount).setScale(currency.getScale(), currency.getRoundingMode()); } public Money add(Money other) { assertSameCurrency(other); return new Money(amount.add(other.amount), currency); } public Money subtract(Money other) { assertSameCurrency(other); return new Money(amount.subtract(other.amount), currency); } private void assertSameCurrency(Money other) { if (! other.currency.equals(this.currency)) { throw new IllegalArgumentException("Money objects must have the same currency");  } } public boolean equals(Object o) { // Check that the currency and amount are the same } public int hashCode() { // Calculate hash code based on currency and amount } public int compareTo(Money other) { // Compare based on currency and amount } }Copy the code

Java with a StreetAddress value object and corresponding designer might look like this (the code is untested, some method implementations omitted for clarity) :

StreetAddress.java

public class StreetAddress implements Serializable.Comparable<StreetAddress> {
    private final String streetAddress;
    private final PostalCode postalCode; // PostalCode is another value object
    private final String city;
    private final Country country; // Country is an enum

    public StreetAddress(String streetAddress, PostalCode postalCode, String city, Country country) {
        // Verify that required parameters are not null
        // Assign the parameter values to their corresponding fields
    }

    // Getters and possible business logic methods omitted

    public boolean equals(Object o) {
        // Check that the fields are equal
    }

    public int hashCode(a) {
        // Calculate hash code based on all fields
    }

    public int compareTo(StreetAddress other) {
        // Compare however you want
    }

    public static class Builder {

        private String streetAddress;
        private PostalCode postalCode;
        private String city;
        private Country country;

        public Builder(a) { // For creating new StreetAddresses
        }

        public Builder(StreetAddress original) { // For "modifying" existing StreetAddresses
            streetAddress = original.streetAddress;
            postalCode = original.postalCode;
            city = original.city;
            country = original.country;
        }

        public Builder withStreetAddress(String streetAddress) {
            this.streetAddress = streetAddress;
            return this;
        }

        // The rest of the 'with... ' methods omitted

        public StreetAddress build(a) {
            return newStreetAddress(streetAddress, postalCode, city, country); }}}Copy the code

entity

The second important concept and sibling in tactical DDD is entity. Entities are objects that are important to their identity. In order to be able to determine the identity of an entity, each entity has a unique ID, which is assigned when the entity is created and remains constant throughout its life.

Two entities with the same type and the same ID are treated as the same entity even if all other attributes are different. Similarly, two entities with the same type and the same attributes but different ids are considered different entities, just as two individuals with the same name are considered different.

In contrast to value objects, entities are mutable. However, this does not mean that you should create setter methods for each property. Try to model all state change operations as verbs corresponding to business operations. The setter will only tell you which properties to change, not why. For example, suppose you have an EmploymentContract entity that has an endDate attribute. Employment contracts may be terminated because they are only temporary, as a result of transfer from within one branch of the company to another, because the employee resigns or because the employer has dismissed the employee. In all of these cases, the endDate changed, but for different reasons. In addition, depending on the reason for termination, other measures may be required. A terminateContract(Reason, finalDay) method already says a lot, not just a setEndDate(finalDay) method.

Having said that, the setter still has a place in DDD. In the example above, there might be a private setEndDate(..) Method to ensure that the end date is set after the start date. This setting method will be used by other entity methods but will not be exposed to the outside world. For describing the master and reference data of an entity without changing its business state, it makes more sense to use a setter than to try to tune the action to a verb. setDescription(..) It can be said that a method called describe is more readable than .

I will illustrate this point with another example. Suppose you have a Person representing the entity of a Person. This person has firstName and lastName assets. Now, if this were just a simple address book, you would let the user change this information as needed, and you could use the setFirstName(..) And setLastName (..) . However, if you are setting up an official government register of citizens, it is easier to involve name changes. You might get a similar result changeName(firstName, lastName, Reason, effectiveAsOfDate). Again, context is everything.

Notes about Getter are introduced into Java getters as part of the JavaBean specification. The specification did not exist in the first version of Java, which is why you can find methods in the standard Java API that do not conform to the specification (such as String.length() versus String.getLength())).

Personally, I would like to see support for real estate in Java. Although they may be using getters and behind the scenes, I want to access property values in the same way as if it were just a generic field: myContact.phonenumber. We can’t do this in Java yet, but we can get pretty close by omitting the get getter suffix. In my opinion, this makes the code smoother, especially if you need to drill down into the object hierarchy to get something, myContact.address ().streetNumber().

But there’s another downside to getting rid of inhalers, and that’s tool support. All Java ides and many libraries rely on the JavaBean standard, which means you may end up manually writing code that might have been automatically generated for you, adding comments that could have been avoided by following conventions.

Entity or value object?

It’s not always easy to know whether to model something as a value object or an entity. Exactly the same real-world concepts can be modeled as entities in one context and value objects in another. Let’s take street addresses as an example.

If you are building an invoice system, the street address is what you print on the invoice. It doesn’t matter what object instance is used as long as the text on the invoice is correct. In this case, the street address is a value object.

If you are setting up a system for a utility, you need to know exactly what kind of gas line or electricity line a given apartment is running. In this case, the street address is an entity and can even be divided into smaller entities, such as buildings or apartments.

Value objects are immutable and small, and therefore easier to use. Therefore, you should aim for a design with few entities and many value objects.

Code example

An entity in Person might look like this (untested code, which omits some method implementations for clarity) :

Person.java

public class Person {

    private final PersonId personId;
    private final EventLog changeLog;

    private PersonName name;
    private LocalDate birthDate;
    private StreetAddress address;
    private EmailAddress email;
    private PhoneNumber phoneNumber;

    public Person(PersonId personId, PersonName name) {
        this.personId = Objects.requireNonNull(personId);
        this.changeLog = new EventLog();
        changeName(name, "initial name");
    }

    public void changeName(PersonName name, String reason) {
        Objects.requireNonNull(name);
        this.name = name;
        this.changeLog.register(new NameChangeEvent(name), reason);
    }

    public Stream<PersonName> getNameHistory(a) {
        return this.changeLog.eventsOfType(NameChangeEvent.class).map(NameChangeEvent::getNewName);
    }

    // Other getters omitted

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o == null|| o.getClass() ! = getClass()) {return false;
        }
        return personId.equals(((Person) o).personId);
    }

    public int hashCode(a) {
        returnpersonId.hashCode(); }}Copy the code

Some things to note in this example:

  • Value object -PersonId- for entity ID. We could have used a UUID, string, or long, but the value object would immediately tell us that this is an ID that identifies a particular Person.

  • In addition to the entity ID, the entity uses a number of other value objects: PersonName, LocalDate (yes, this is a value object even though it’s part of the standard Java API), StreetAddress, EmailAddress, and PhoneNumber.

  • Instead of using a change method to change the name, we used a business method that also stores the change and the reason for the change in the event log.

  • There is an getter for obtaining the name change history.

  • Equals and hashCode only check entity ids.

Domain-driven design and CRUD

We are now at the point where we can resolve the issues related to DDD and CRUD. CRUD stands for Create, Retrieve, Update, and Delete, and is also a common UI pattern in enterprise applications:

  • The main view consists of a grid, possibly with filtering and sorting capabilities, in which you can retrieve entities.

  • In the main view, there is a button to create a new entity. Clicking this button will pop up an empty form, and after submitting the form, the new entity will be displayed in the grid (CREATE).

  • In the main view, there is a button to edit the selected entity. Clicking this button brings up a form containing entity data. After the form is submitted, the entity is updated with new information.

  • In the main view, there is a button to delete the selected entity. Clicking this button deletes entities from the grid.

This pattern certainly has its place, but should be the exception, not the norm, in domain-driven applications. Here’s why: CRUD applications are only used to structure, display, and edit data. It usually does not support the underlying business process. When a user enters something into the system, changes or deletes something, there is a business reason behind the decision. Perhaps the change was made as part of a larger business process? In CRUD systems, the reason for the change is lost and the business process is on the user’s head.

A true domain-driven user interface would be based on operations that are themselves part of the universal language (and therefore the domain model), and the business processes are built into the system, not the user head. This, in turn, leads to systems that are more robust, but arguably less flexible, than pure CRUD applications. I’ll illustrate this difference with an ironic example:

Company A has A domain-driven employee management system, while Company B has A CRUD-driven approach. One employee resigned at both companies. The following occurs:

  • Company A:

  • The manager looked up employee records in the system.

  • The manager chooses the “Terminate employment Contract” operation.

  • The system requires a termination date and reason.

  • The manager enters the required information and then clicks Terminate.

  • The system automatically updates employee records, revokes employee user credentials and e-office keys, and sends notifications to the payroll system.

  • Company B:

  • The manager looked up employee records in the system.

  • The manager adds a check and enters the termination date in the Contract Termination check box, and then clicks Save.

  • The administrator logs in to the user management system, searches for user accounts, checks the Disabled check box, and then clicks Save.

  • The manager logs in to the office key management system, looks for the user’s key, checks the Disabled check box, and then clicks Save.

  • The manager sent an E-mail to the payroll department informing them that the employee had resigned.

The key points are as follows: Not all applications are suitable for domain-driven design, and domain-driven applications have not only domain-driven backends, but domain-driven user interfaces.

The aggregation

Now, when we know what entities and value objects are, we’ll look at the next important concept: aggregation. A collection is a group of entities and value objects with certain characteristics:

  • Aggregations are created, retrieved, and stored as a whole.

  • Aggregation is always in a consistent state.

  • The aggregation is owned by an entity called the aggregation root, whose ID is used to identify the aggregation itself.

In addition, there are two important limitations on aggregation:

  • An aggregate can only be referenced externally through its root. Objects outside the aggregation may not be able to reference any other entities inside the aggregation.

  • The aggregation root is responsible for enforcing business invariants within the aggregation to ensure that the aggregation is always in a consistent state.

This means that every time you design an entity, you have to decide what type of entity you want to create: will it act as an aggregation root, or what I call a local entity that sits inside and below the aggregation? The head of supervision? Since local entities cannot be referenced from outside the aggregation, it is sufficient that their ids are unique within the aggregation (they have a local identity), and that the aggregation roots must have a globally unique ID (they have a global identity). However, the importance of this semantic difference varies depending on how you choose to store the aggregation. In a relational database, it makes the most sense to use the same primary key generation mechanism for all entities. On the other hand, if the entire aggregation is saved as a single document in a document database, it makes more sense to use true local ids for local entities.

So how do you know if an entity is an aggregate root? First, the fact that there is a parent-child (or master-slave) relationship between two entities does not automatically turn the parent into an aggregate root and the child into a local entity. More information is needed before that decision can be made. Here’s how I do it:

  • How do I access entities in my application?

  • If an entity is to be found by ID or by some kind of search, that entity might be an aggregation root.

  • Do other summaries need to reference it?

  • If the entity is to be referenced from another aggregation, it must be the aggregation root.

  • How do I modify entities in my application?

  • If it can be modified independently, it may be the aggregate root.

  • If you cannot modify another entity without changing it, it may be a local entity.

Knowing that you want to create an aggregation root, how do you make it enforce business invariants, and what does that even mean? Business invariance is a rule that must always hold regardless of what happens with the aggregation. A simple business invariance might be that the total amount in the invoice must always be the sum of the order items, regardless of whether the items are added, edited, or deleted. Invariants should be part of the universal language and domain model.

Technically, an aggregation root can enforce business invariants in different ways:

  • All state change operations are performed through the aggregate root.

  • State change operations are allowed on local entities, but they notify the aggregation root whenever a change is made.

In some cases, such as in the example with invoice totals, invariants can be enforced by having the pool root calculate the total dynamically for each request total.

I designed the aggregation myself so that invariants can be enforced anytime, anywhere. Arguably, the same end result can be achieved by introducing strict data validation (the Java EE approach) that is performed before saving the aggregation. Ultimately, it’s a matter of personal preference.

General design criteria

There are certain guidelines to follow when designing aggregations. I choose to call them guidelines rather than rules, because in some cases it is necessary to break them.

Rule 1: Keep aggregation small

Aggregation is always retrieved and stored as a whole. The less data you have to read and write, the better the system will perform. For the same reason, you should avoid unrestricted one-to-many associations (collections) because they grow over time.

Smaller aggregations also make it easier for aggregation roots to enforce business invariants, even if you prefer to use value objects (immutable) rather than local entities (mutable) in aggregation.

Rule 2: Reference other summaries by ID

Create a value object that wraps the ID of the aggregation root and uses it as a reference rather than referring directly to another aggregation. This makes it easier to maintain aggregation consistency boundaries, because you can’t even accidentally change the state of one aggregation from another. It also prevents the retrieval of the deep tree of objects from the data store when the aggregation is retrieved.

If you do need access to other aggregated data, and there is no better way to resolve the problem, you may need to violate this guideline. You can rely on the lazy loading capabilities of persistence frameworks, but in my experience they tend to cause more problems than they solve. A more coded but more explicit approach is to take the repository (more on that later) as a method parameter:

public class Invoice extends AggregateRoot<InvoiceId> {

    private CustomerId customerId;

    // All the other methods and fields omitted

    public void copyCustomerInformationToInvoice(CustomerRepository repository) {
        Customer customer = repository.findById(customerId);
        setCustomerName(customer.getName());
        setCustomerAddress(customer.getAddress());
        // etc.}}Copy the code

In any case, you should avoid a two-way relationship between aggregations.

Rule 3: Change only one aggregation per transaction

Try to design your actions so that you change only one aggregation in a single transaction. For operations that span multiple aggregations, use domain events and final conformance (more on this later). This prevents unexpected side effects and makes it easier to distribute the system if needed in the future. In addition, it makes it easier to use document databases without transaction support.

However, this introduces increased complexity. You need to set up the infrastructure to reliably handle domain events. Especially in a monolithic application where you can schedule domain events synchronously in the same thread and transaction, this added complexity is rarely, in my opinion, motivated. I think a good compromise is to still rely on domain events to change the other aggregations, but in the same transaction:

In any case, you should avoid avoiding changing the state of an aggregation directly from another aggregation.

We’ll talk more about this later when we discuss domain events.

Rule 4: Use optimistic locking

The main function of aggregation is to enforce business invariance and always ensure data consistency. If the aggregation ends up broken due to data store update conflicts, this is all in vain. Therefore, when saving aggregates, open locking should be used to prevent data loss.

Optimistic locking is superior to pessimistic locking because persistence frameworks are easy to implement and distribute and extend if they are not out of the box.

Sticking to the first rule would also help here, since small totals (and therefore small transactions) also reduce the risk of conflict.

Aggregation, invariants, UI binding and validation

Some of you may now be wondering how aggregation and force business invariants work with the user interface, and more specifically with form binding. If invariants are always enforced, and aggregation must always be in a consistent state, what do you do when a user fills out a form? Also, how do you bind form fields to aggregation without a setter?

There are several ways of dealing with this problem. The simplest solution is to postpone the implementation of immutable until you save the aggregation, add setters for all properties, and then bind entities directly to the form. I personally don’t like this approach because I think it’s data-driven rather than domain-driven. There is a high risk that entities will degrade to anemic holders of data, and that business logic will eventually reach the service layer (or worse, in the UI).

Instead, I prefer the other two approaches. The first is to model the form and its contents as your own domain model concept. In the real world, if you want to apply for something, you usually have to fill out an application form and submit it. The application is then processed, and once all the necessary information is provided and you meet the rules, the application is granted whatever you request. You can simulate this process in the domain model. For example, if you have a Membership aggregation root, you can also have a MembershipApplication aggregation root that collects all the information Membership needed to create it. You can then use the application object as input when you create the membership object.

The second approach is a variation of the first approach, which is the essential pattern. For each entity or value object that you want to edit, create a mutable essence object that contains the same information. This base object is then bound to the form. Once the base object contains all the necessary information, it can be used to create a real entity or value object. Unlike the first approach, essential objects are not part of the domain model; they are merely technical constructs of existence that make it easier to interact with real domain objects. In practice, the basic pattern might look like this:

Person.java

public class Person extends AggregateRoot<PersonId> {

    private final DateOfBirth dateOfBirth;
    // Rest of the fields omitted

    public Person(String firstName, String lastName, LocalDate dateOfBirth) {
        setDateOfBirth(dateOfBirth);
        // Populate the rest of the fields
    }

    public Person(Person.Essence essence) {
        setDateOfBirth(essence.getDateOfBirth());
        // Populate the rest of the fields
    }

    private void setDateOfBirth(LocalDate dateOfBirth) {
        this.dateOfBirth = Objects.requireNonNull(dateOfBirth, "dateOfBirth must not be null");
    }

    @Data // Lombok annotation to automatically generate getters and setters
    public static class Essence {
        private String firstName;
        private String lastName;
        private LocalDate dateOfBirth;
        private String streetAddress;
        private String postalCode;
        private String city;
        private Country country;

        public Person createPerson(a) {
            validate();
            return new Person(this);
        }

        private void validate(a) {
            // Make sure all necessary information has been entered, throw an exception if not}}}Copy the code

Code example

Here is an example of an aggregation root (Order) and a local entity (OrderItem) with fundamental identity (this code is untested; some method implementations have been omitted for clarity) :

Order.java

public class Order extends AggregateRoot<OrderId> { // ID type passed in as generic parameter

    private CustomerId customer;
    private String shippingName;
    private PostalAddress shippingAddress;
    private String billingName;
    private PostalAddress billingAddress;
    private Money total;
    private Long nextFreeItemId;
    private List<OrderItem> items = new ArrayList<>();

    public Order(Customer customer) {
        super(OrderId.createRandomUnique());
        Objects.requireNonNull(customer);

        // These setters are private and make sure the passed in parameters are valid:
        setCustomer(customer.getId());
        setShippingName(customer.getName());
        setShippingAddress(customer.getAddress());
        setBillingName(customer.getName());
        setBillingAddress(customer.getAddress());

        nextFreeItemId = 1L;
        recalculateTotals();
    }

    public void changeShippingAddress(String name, PostalAddress address) {
        setShippingName(name);
        setShippingAddress(address);
    }

    public void changeBillingAddress(String name, PostalAddress address) {
        setBillingName(name);
        setBillingAddress(address);
    }

    private Long getNextFreeItemId(a) {
        return nextFreeItemId++;
    }

    void recalculateTotals(a) { // Package visibility to make the method accessible from OrderItem
        this.total = items.stream().map(OrderItem::getSubTotal).reduce(Money.ZERO, Money::add);
    }

    public OrderItem addItem(Product product) {
        OrderItem item = new OrderItem(getNextFreeItemId(), this);
        item.setProductId(product.getId());
        item.setDescription(product.getName());
        this.items.add(item);
        return item;
    }

    // Getters, private setters and other methods omitted
}
Copy the code

OrderItem.java

public class OrderItem extends LocalEntity<Long> { // ID type passed in as generic parameter

    private Order order;
    private ProductId product;
    private String description;
    private int quantity;
    private Money price;
    private Money subTotal;

    OrderItem(Long id, Order order) {
        super(id);
        this.order = Objects.requireNonNull(order);
        this.quantity = 0;
        this.price = Money.ZERO;
        recalculateSubTotal();
    }

    private void recalculateSubTotal(a) {
        Money oldSubTotal = this.subTotal;
        this.subTotal = price.multiply(quantity);
        if(oldSubTotal ! =null && !oldSubTotal.equals(this.subTotal)) {
            this.order.recalculateTotals(); // Invoke aggregate root to enforce invariants}}public void setQuantity(int quantity) {
        if (quantity < 0) {
            throw new IllegalArgumentException("Quantity cannot be negative");
        }
        this.quantity = quantity;
        recalculateSubTotal();
    }

    public void setPrice(Money price) {
        Objects.requireNonNull(price, "price must not be null");
        this.price = price;
        recalculateSubTotal();
    }

    // Getters and other setters omitted
}
Copy the code

Field events

So far, we have only looked at “things” in the domain model. However, these can only be used to describe the static state of the model at any given moment. In many business models, you also need to be able to describe what happens and change the state of the model. To do this, you can use domain events.

Domain events are not included in Evans’ book on domain-driven design. They were later added to the toolbox and included in Vernon’s book.

A domain event is anything that happens in the domain model that might be of interest to other parts of the system. Domain events can be coarse-grained (for example, creating a specific aggregate root or starting a process) or fine-grained (for example, changing a specific attribute of a specific aggregate root).

Domain events typically have the following characteristics:

  • They are immutable (after all, you can’t change the past).

  • They have time stamps from the time of the incident.

  • They may have unique ids that help distinguish one event from another. This depends on the type of event and how events are distributed.

  • They are published through aggregate root or domain services (more on that later).

After a domain event is published, one or more domain event listeners can receive the event, which in turn may trigger other processing, new domain events, and so on. The publisher does not know what will happen to the event, and the listener should not be aware of the ability to affect the publisher (in other words, publishing domain events should have no side effects from the publisher’s perspective). Therefore, it is recommended that domain event listeners not run in the same transaction that publishes the event.

From a design perspective, the biggest advantage of domain events is that they make the system extensible. You can add as many domain event listeners as you need to trigger new business logic without having to change existing code. Naturally, assume that the correct event is published first. You may be aware of some events in advance, but others may show up later. Of course, you can try to guess what types of events will be needed and add them to the model, but then you run the risk of clogging the system with domain events that aren’t used anywhere. A better approach is to publish domain events as easily as possible, and then add missing events when you realize you need them.

An event source is a design pattern in which system state persists as an ordered log of events. Each event even changes the system state, and the current state can be calculated at any time by replaying the event log from beginning to end. This pattern is particularly useful in applications such as financial ledgers or medical records, where history is just as important (or more important) as current state.

In my experience, most parts of a typical business system do not need event sources, but some do. In my opinion, forcing the entire system to use event sources as a persistence model would be too big. However, I found that I could use domain events to implement event sources where needed. In practice, this means that each operation that changes model state also publishes domain events stored in some event log. Technically, this is beyond the scope of this article.

Distribution domain events

Use domain events only if you have a reliable way to distribute domain events to listeners. Inside the whole, you can use the standard observer pattern to handle distribution in memory. But even in this case, if you follow the good practice of running event publishers in separate transactions, you may need something more complicated. What if one of the event listeners fails and you have to resend the event?

Vernon suggests two different ways to distribute events that can be run remotely and locally. I encourage you to read his book for details, but HERE I’ll briefly cover the options.

Distributed via message queues

This solution requires an external messaging solution (MQ), such as AMQP or JMS. The solution needs to support a publish-subscribe model and guaranteed delivery. When a domain event is published, the producer sends it to MQ. Domain event listeners subscribe to MQ and will be notified immediately.

The advantages of this model are that it is fast, easy to implement, and relies on existing reliable messaging solutions. The downside is that you have to set up and maintain the MQ solution, and you cannot receive past events if a new consumer subscribes.

Distribution via event logs

This solution requires no other components, but some coding. Once a domain event is published, it is attached to the event log. The domain event listener polls this log periodically to check for new events. They also keep track of events that have been processed to avoid having to traverse the entire event log every time.

The advantage of this model is that it does not require any other components and contains a complete event history that can be replayed against new event listeners. The disadvantages are that there is some work to be done, and the delay between events that the listener publishes and receives is at best a polling interval.

Data consistency is always a challenge in situations where distributed systems or multiple data stores participate in the same logical transaction. Advanced application servers support distributed transactions that can be used to solve this problem, but they require specialized software and can be complex to configure and maintain. If absolute consistency is an absolute requirement, you have no choice but to use distributed transactions, but in many cases, you may find that high consistency isn’t really that important from a business perspective. We’ve been used to thinking about strong consistency since the time we used a single application to communicate with a single database in a single ACID transaction.

Ultimate consistency is an alternative to strong consistency. This means that the data in the application will eventually become consistent, but sometimes all parts of the system are not in sync with each other, which is perfectly fine. Designing applications for ultimate consistency requires a different way of thinking, but this in turn leads to a system that is more resilient and extensible than a system that requires only strong consistency.

In domain-driven systems, domain events are an excellent way to achieve final consistency. Any system or module that needs to update itself when something happens in another module or system can subscribe to domain events from that system:

In the example above, any changes made to system A will eventually be propagated to systems B, C, and D through domain events. Each system will actually update the data store using its own local transactions. Depending on the event distribution mechanism and the load of the system, the scope of the travel time can never to a second (all systems running in the same network, event will immediately pushed to subscribers) to a few hours or even days (in some cases, the system is in offline state, only occasionally connected to the network to download all domains of events since the last signing).

To achieve ultimate consistency successfully, you must have a reliable system to distribute valid domain events, even if some subscribers are not currently online when the event is first published. You also need to design your business logic and user interface around the assumption that any data can become obsolete over time. You also need to set limits on how long data inconsistencies can occur. You may be surprised to learn that some data may remain inconsistent for days, while others must be updated in seconds or less.

Code example

The following is an example of the summary root (Order), which publishes the domain event () when the OrderShipped Order is published. The domain listener (InvoiceCreator) receives the event and creates a new invoice in a separate transaction. Suppose there is a mechanism to publish all registered events while saving the aggregate root (untested code, some method implementations omitted for clarity) :

OrderShipped.java

public class OrderShipped implements DomainEvent {
    private final OrderId order;
    private final Instant occurredOn;

    public OrderShipped(OrderId order, Instant occurredOn) {
        this.order = order;
        this.occurredOn = occurredOn;
    }

    // Getters omitted
}
Copy the code

Order.java

public class Order extends AggregateRoot<OrderId> {

    // Other methods omitted

    public void ship(a) {
        // Do some business logic
        registerEvent(new OrderShipped(this.getId(), Instant.now())); }}Copy the code

InvoiceCreator.java

public class InvoiceCreator {

    final OrderRepository orderRepository;
    final InvoiceRepository invoiceRepository;

    // Constructor omitted

    @DomainEventListener
    @Transactional
    public void onOrderShipped(OrderShipped event) {
        var order = orderRepository.find(event.getOrderId());
        varinvoice = invoiceFactory.createInvoiceFor(order); invoiceRepository.save(invoice); }}Copy the code

Movable and static objects

Before I continue, I want to introduce you to movable and static objects. These are not really DDD terms, but rather things I use when thinking about different parts of the domain model. In my world, a removable object is any object that can have multiple instances and can be passed between different parts of an application. Value objects, entities, and domain events are moveable objects.

A static object, on the other hand, is a singleton (or pooled resource) that is always in one place and called by other parts of the application, but rarely passed (unless injected into other static objects). Repositories, domain services, and factories are static objects.

This difference is important because it determines what relationships can be established between objects. Static objects can hold references to other static and moveable objects.

Moveable objects can hold references to other moveable objects. However, moveable objects can never hold references to static objects. If a movable object needs to interact with a static object, the static object must be passed as a method parameter to the method that will interact with it. This makes the movable objects more portable and independent, because you don’t have to look up and inject references to static objects into the movable objects every time you deserialize them.

Other domain objects

When you use domain-driven code, you sometimes encounter situations where classes don’t actually fit into value objects, entities, or domain event models. In my experience, this usually happens when:

Any information from the external system (= another bounded context). From your point of view, this information is immutable, but it has a global ID that uniquely identifies it.

Type data used to describe other entities (Vaughn Vernon calls these objects standard types). These objects have global ids and may even be mutable to some extent, but they are immutable for all practical purposes of the application itself.

Framework/infrastructure-level entities for storing audit entries or domain events in a database, for example. Depending on usage, they may or may not have a global ID, and may or may not be variable.

My approach to these cases is to use a hierarchy of base classes and interfaces beginning with A, DomainObject. A domain object is any movable object associated with a domain model. If an object is purely a value object, or not a pure entity, I can declare it as a domain object, explain what it does and why in JavaDocs, and move on.

I like to use interfaces at the top of the hierarchy because you can combine them any way you like, even enums. Some interfaces are tag interfaces without any methods that simply indicate what role the implementation class plays in the domain model. In the figure above, the classes and interfaces are as follows:

  • DomainObject – Top-level markup interface for all domain objects.

  • DomainEvent- Interface for all domain events. It usually contains some metadata about the event, such as the date and time of the event, but it can also be a markup interface.

  • ValueObject- Marking interface for all value objects. The implementation of the interface must be immutable and must implement equals() and hashCode(). Unfortunately, even if that were good, it cannot be enforced at the interface level.

  • IdentifiableDomainObject- An interface for all domain objects that can be uniquely identified in some cases. I often design this as a generic interface with ID type as a generic parameter.

  • StandardType – StandardType markup interface.

  • Entity- An abstract base class for an Entity. I often enter ID in the field, equals() and hashCode() accordingly. Depending on the persistence framework, I might also add optimistic locking information to this class.

  • LocalEntity- the abstract base class for the LocalEntity. If I use local identity against a local entity, the class will contain code to manage local identity. Otherwise, it might just be an empty tag class.

  • AggregateRoot- The abstract base class for the aggregation root. If I use a local identity for a local entity, the class will contain code to generate a new local ID. The class will also contain code for scheduling domain events. If the optimistic lock information is not included in the Entity class, it must be included here. Audit information (created, last updated, etc.) can also be added to this class, depending on the requirements of the application.

Code example

In this code example, we have two bounded contexts, identity management and employee management:

The employee management context requires some, but not all, information about users in the identity management context. There is a REST endpoint for this, and the data is serialized to JSON.

In the identity management context, aUser is represented as follows:

User.java (Identity Management)

public class User extends AggregateRoot<UserId> {
    private String userName;
    private String firstName;
    private String lastName;
    private Instant validFrom;
    private Instant validTo;
    private boolean disabled;
    private Instant nextPasswordChange;
    private List<Password> passwordHistory;

    // Getters, setters and business logic omitted
}
Copy the code

In the employee management context, we only need the user ID and name. The user is uniquely identified by ID, but the name is displayed in the UI. We obviously can’t change any user information, so user information is immutable. The code is as follows:

User.java (Employee Management)

public class User implements IdentifiableDomainObject<UserId> {
    private final UserId userId;
    private final String firstName;
    private final String lastName;

    @JsonCreator // We can deserialize the incoming JSON directly into an instance of this class.
    public User(String userId, String firstName, String lastName) {
        // Populate fields, convert incoming userId string parameter into a UserId value object instance.
    }

    public String getFullName(a) {
        return String.format("%s %s", firstName, lastName);
    }

    // Other getters omitted.

    public boolean equals(Object o) {
        // Check userId only
    }

    public int hashCode(a) {
        // Calculate based on userId only}}Copy the code

The repository

Now that we’ve covered all the movable objects of the domain model, it’s time to move on to static objects. The first static object is a repository. Repositories are persistent containers for aggregation. Even if the system is restarted, all the aggregations saved to the repository can be retrieved from there later.

At a minimum, the repository should have the following capabilities:

  • The ability to hold aggregations as a whole in some type of data store

  • The ability to retrieve the aggregate as a whole based on its ID

  • The ability to remove aggregates as a whole based on their ID

In most cases, the repository needs more advanced query methods to actually use it.

In effect, a repository is a domain-aware interface to external data stores, such as relational databases, NoSQL databases, directory services, and even file systems. Even if the actual storage is hidden behind a repository, its storage semantics often leak out and limit the appearance of the repository. Therefore, repositories are typically collections oriented or persistence-oriented.

Collection-oriented repositories are designed to simulate in-memory collections of objects. After you add a collection to a collection, any changes you make to it are automatically retained until the collection is deleted from the repository. In other words, a collection-oriented repository will have methods such as add() and remove(), but no methods for saving.

Persistence-oriented repositories, on the other hand, do not attempt to mimic collections. Instead, it acts as a facade in an external persistence solution and contains methods such as INSERT (), update(), and delete(). Any changes made to the aggregation must be explicitly saved to the repository by calling the update() method.

It is important to ensure that the repository types are correct at the beginning of the project, because they are semantically completely different. In general, persistence-oriented repositories are easier to implement and can be used with most existing persistence frameworks. Unless an underlying persistence framework is out of the box, aggregation-oriented repositories are more difficult to implement.

Code example

This example demonstrates the difference between a collection-oriented repository and a persistence-oriented repository.

Collection-oriented repositories

public interface OrderRepository {

    Optional<Order> get(OrderId id);

    boolean contains(OrderID id);

    void add(Order order);

    void remove(Order order);

    Page<Order> search(OrderSpecification specification, int offset, int size);
}


// Would be used like this:

public void doSomethingWithOrder(OrderId id) {
    orderRepository.get(id).ifPresent(order -> order.doSomething());
    // Changes will be automatically persisted.
}
Copy the code

Persistence-oriented repositories

public interface OrderRepository {

    Optional<Order> findById(OrderId id);

    boolean exists(OrderId id);

    Order save(Order order);

    void delete(Order order);

    Page<Order> findAll(OrderSpecification specification, int offset, int size);
}


// Would be used like this:

public void doSomethingWithOrder(OrderId id) {
    orderRepository.findById(id).ifPresent(order -> {
        order.doSomething();
        orderRepository.save(order);
    });
}
Copy the code

The annotation repository on CQRS always holds and retrieves the complete aggregation. This means that they can be very slow, depending on how they are implemented and the size of the object graph that must be constructed for each aggregation. This can be problematic from a UX perspective, especially with two use cases in mind. The first is a small list in which you want to display the aggregation list, but only with one or two properties. Displaying the full object graph when only a few property values are needed wastes time and computing resources, and generally results in a slow user experience. In another case, you need to merge data from multiple aggregations to display a single item in a list. This can lead to worse performance.

As long as the data sets and aggregations are small, the performance penalty may be acceptable, but there is a solution when the performance is not acceptable at all: command query separation of responsibility (CQRS).

CQRS is a mode where you can completely separate write (command) and read (query) operations from each other. The details are beyond the scope of this article, but for DDD, you should use the following pattern:

  • All user actions that change the state of the system pass through the repository normally.

  • All queries bypass the repository and go directly to the underlying database, fetching only the data needed and nothing else.

  • If desired, you can even design separate query objects for each view in the user interface

  • The data transfer object (DTO) returned by the query object must contain the aggregation ID so that the correct aggregation can be retrieved from the repository if changes are needed.

In many projects, you might end up using CQRS in some views and directly using repository queries in others.

Field service

We’ve already mentioned that both value objects and entities can (and should) contain business logic. However, in some cases, a logic simply does not work for a particular value object or a particular entity. Putting business logic in the wrong place is a bad idea, so we need another solution. Enter our second static object: the domain service.

Domain services have the following characteristics:

  • They are stateless

  • They are highly cohesive (meaning they focus on one thing and one thing only)

  • They contain business logic that doesn’t fit naturally elsewhere

  • They can interact with other domain services and, to some extent, with the repository

  • They can publish domain events

In its simplest form, a domain service can be a utility class that contains static methods. More advanced domain services can be implemented as singletons with other domain services and repositories

Domain services should not be confused with application services. In the next article in this series, we’ll take a closer look at application services, but in short, application services act as intermediaries between the isolated domain model and the rest of the world. Application services are responsible for handling transactions, ensuring system security, finding the appropriate aggregation, calling methods on it, and saving changes back to the database. The application services themselves contain no business logic.

You can summarize the difference between application services and domain services as follows: Domain services are only responsible for making business decisions, whereas application services are only responsible for choreography (finding the right objects and calling the right methods in the right order). Therefore, domain services should generally not call any repository methods that change the state of the database – this is the responsibility of the application services.

Code example

In the first example, we will create a realm service to check whether certain currency transactions are allowed. The implementation has been greatly simplified, but it is clear that business decisions can be made based on some predefined business rules.

In this case, because the business logic is so simple, you might already be able to add it directly to the Account class. However, once the more advanced business rules are in effect, you can make decisions in your own classes (especially if the rules change over time or depend on some external configuration). Another obvious sign that this logic might belong to a domain service is that it involves multiple aggregations (two accounts).

TransactionValidator.java

public class TransactionValidator {

    public boolean isValid(Money amount, Account from, Account to) {
        if(! from.getCurrency().equals(amount.getCurrency())) {return false;
        }
        if(! to.getCurrency().equals(amount.getCurrency())) {return false;
        }
        if (from.getBalance().isLessThan(amount)) {
            return false;
        }
        if (amount.isGreaterThan(someThreshold)) {
            return false;
        }
        return true; }}Copy the code

In the second example, we will examine domain services with special capabilities: their interfaces are part of the domain model, but their implementations are not. This might happen when you need external information to make business decisions within the domain model, but you are not interested in where the information comes from.

CurrencyExchangeService.java

public interface CurrencyExchangeService {

    Money convertToCurrency(Money currentAmount, Currency desiredCurrency);
}
Copy the code

When domain models are wired together, for example using a dependency injection framework, you can inject the correct implementation of the interface. You might have one calling the local cache, another calling the remote Web service, a third just for testing, and so on.

The factory

The last static object we’ll look at is factory. As the name suggests, the factory is responsible for creating new aggregations. However, this does not mean that you need to create a new factory for each aggregation. In most cases, the constructor of the aggregate root is sufficient to set up the aggregate in a consistent state. You usually need a separate factory when:

Business logic participates in the creation of collections

The structure and content of the aggregation can vary greatly depending on the input data

The input data is so large that the builder pattern (or something similar) is required

The factory is transitioning from one limited context to another

A factory can be a static factory method on an aggregate root class or a separate factory class. Factories can interact with other factories, repositories, and domain services, but must not change the state of the database (and therefore cannot be saved or deleted).

Code example

In this example, we will look at a factory that transforms between two bounded contexts. In the shipping context, the customer is no longer called the customer, but the shipping recipient. The customer ID is still stored so that we can later link the two concepts together as needed.

ShipmentRecipientFactory.java

public class ShipmentRecipientFactory {
    private final PostOfficeRepository postOfficeRepository;
    private final StreetAddressRepository streetAddressRepository;

    // Initializing constructor omitted

    ShipmentRecipient createShipmentRecipient(Customer customer) {
        var postOffice = postOfficeRepository.findByPostalCode(customer.postalCode());
        var streetAddress = streetAddressRepository.findByPostOfficeAndName(postOffice, customer.streetAddress());
        var recipient = new ShipmentRecipient(customer.fullName(), streetAddress);
        recipient.associateWithCustomer(customer.id());
        returnrecipient; }}Copy the code

Module,

It’s almost time to read the next article, but before we leave tactical domain-driven design, we need to explore another concept, modules.

Modules in DDD correspond to packages in Java and namespaces in C#. A module can correspond to one bounded context, but in general, a bounded context will have multiple modules.

Classes that belong together should be grouped into the same module. However, you should not create modules based on the type of classes, but on how those classes fit into the domain model from a business perspective. That is, you should not put all the warehouses into one module, all the entities into another, etc. Instead, you should incorporate all classes that relate to a particular aggregation or a particular business process into the same module. This makes navigation code much easier, because classes that belong to the same class and work together also live together.

The module instance

This is an example of a module structure that groups classes by type. Don’t do this:

  • foo.bar.domain.model.services
  • AuthenticationService
  • PasswordEncoder
  • foo.bar.domain.model.repositories
  • UserRepository
  • RoleRepository
  • foo.bar.domain.model.entities
  • User
  • Role
  • foo.bar.domain.model.valueobjects
  • UserId
  • RoleId
  • UserName

A better approach is to group classes by procedure and summary. Instead, do the following:

  • foo.bar.domain.model.authentication
  • AuthenticationService
  • foo.bar.domain.model.user
  • User
  • UserRepository
  • UserId
  • UserName
  • PasswordEncoder
  • foo.bar.domain.model.role
  • Role
  • RoleRepository
  • RoleId

Why is tactical domain drive design important?

As I mentioned in the introduction to the first article in this series, I first encountered domain-driven design while rescuing a project that was plagued by serious data inconsistencies. In the absence of any domain model or ubiquitous language, we started to transform existing data models into aggregations and data access objects into repositories. Because of these constraints introduced into the software, we managed to get rid of the inconsistencies and were eventually able to deploy the software into production.

My first exposure to tactical domain-driven design proved to me that you can benefit even when all other aspects of your project are not domain-driven. My favorite DDD building blocks I tend to use in all the projects I work on are value objects. It’s easy to introduce, and even the code is easier to read and understand because it brings context to your properties. Immutability also tends to make complex things simpler.

I also often try to split the data model into a summary and a repository, even though the data model is completely bare (just getters and setters without any business logic). This helps to keep data consistent and avoid strange side effects and optimistic locking exceptions when updating the same entity through different mechanisms.

Domain events are useful for decoupling code, but they are a double-edged sword. If you rely too much on events, the code becomes more difficult to understand and debug because it is not immediately clear what other actions a particular event will trigger, or what events will cause a particular action to be triggered in the first place.

Like other software design patterns, tactical domain-driven design provides a solution to a common set of problems, especially when building enterprise software. The more tools you have, the easier it is to solve the problems you will inevitably encounter in your career as a software developer.