Domain-driven design and hexagonal architecture

Translated from: vaadin.com/learn/tutor…

In the first two articles, we learned about strategic and tactical domain-driven design. Now it’s time to learn how to translate domain models into usable software – more specifically, how to do so using a hexagonal architecture.

Although the code examples are written in Java, the first two articles are fairly generic. Although many of the theories in this article can be applied to other environments and languages, I have explicitly written Java and Vaadin for it.

Again, the content is 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. However, even though I have expressed my own thoughts, thoughts and experiences in previous articles, my thoughts and beliefs make this article more vivid. That said, it was Evans and Vernon’s book that got me into DDD in the first place, and I think I think what I’m writing here is not that far from the book.

This is the second edition of this article. In the first one, I got the concept of a port wrong. I appreciate readers pointing this out in the comments. I have now corrected the error and updated the examples and diagrams accordingly. Comments on my interpretation of this architectural style and DDD are always welcome

Why is it called a hexagon?

The name hexagonal structure comes from the way this architecture is often described:

We will return to the reason why hexagon is used later in this article. This architecture is also named for ports and adapters (to better explain the central idea behind them) and onion architecture (due to the way it is layered).

Next, we’ll take a closer look at the Onion. We will start with the core model (the domain model) and work through it one layer at a time until we reach the adapters and the systems and clients that interact with them.

Hexagon vs. Traditional Layers Once we delve into the hexagon architecture, you’ll see some similarities with the more traditional layered architecture. Indeed, you can think of hexagonal architecture as an evolution of layered architecture. However, there are some differences, especially in system interaction with the outside world. To better understand these differences, let’s start with an overview of the layered architecture:

The idea is that the system consists of layers stacked on top of each other. A higher layer can interact with a lower layer, but not vice versa. In general, in a domain-driven layered architecture, the UI layer should be at the top. This layer in turn interacts with the application services layer, which interacts with the domain model residing in the domain layer. At the bottom, we have an infrastructure layer that communicates with external systems, such as databases.

In a hexagon system, you’ll find that the application layer and domain layer are still pretty much the same. However, treat the UI layer and the infrastructure layer in a completely different way. Read on to learn how to do it.

The domain model

At the heart of the hexagonal architecture is the domain model, which is implemented using the tactical DDD building blocks we introduced in the previous article. This is where the so-called business logic resides, where all business decisions are made. This is also the most stable part of the software, and you want it to change the least (unless the business itself changes).

The domain model has been the subject of the first two articles in this series, so we won’t go over it here. However, the domain model alone does not provide any value if you cannot interact with it. To do this, we must move up to the next layer of the Onion.

Application service

Application services act as the basis for clients to interact with the domain model. Application services have the following characteristics:

  • They are stateless

  • They enforce system security

  • They control database transactions

  • They orchestrate business operations but do not make any business decisions (that is, they do not contain any business logic)

Let’s take a closer look at what that means.

Hexagons and entities control boundaries

If you have heard of the entity-Control-Boundary pattern before, you will find the hexagon architecture familiar. You can think of aggregations as entities, domain services, factories, and repositories as controllers, and application services as boundaries.

stateless

The application service does not maintain any internal state that can be changed by interacting with the client. All the information required to perform the operation should be used as input parameters to the application service method. This will make the system simpler, easier to debug and expand.

If you find yourself having to make multiple application service calls within the context of a single business process, you can model the business process in its own classes and pass the instances as input parameters to the application service methods. This method will then come into play and return an updated instance of the business process object, which can in turn be used as input to other application service methods:

Business process as input parameter

public class MyBusinessProcess {
    // Current process state
}

public interface MyApplicationService {

    MyBusinessProcess performSomeStuff(MyBusinessProcess input);

    MyBusinessProcess performSomeMoreStuff(MyBusinessProcess input);
}
Copy the code

You can also make business process objects mutable and have application service methods directly change the state of the object. I personally don’t like this approach because I think it can lead to undesirable side effects, especially if the trade is eventually rolled back. This depends on how the client invokes the application service, which will be returned later in the section on ports and adapters.

For tips on how to implement more complex and longer running business processes, I suggest you read Vernon’s book

Safe to perform

The application service ensures that the current user is allowed to perform the action in question. Technically, you can do this manually at the top of each application service method, or you can use more sophisticated methods such as AOP. It does not matter how security is enforced as long as it occurs at the application services layer and not within the domain model. Now, why does this matter?

When we talk about security in applications, we tend to focus on preventing unauthorized access rather than allowing authorized access. So any security checks we add to the system actually make it harder to use. If we add these security checks to the domain model, we might get into a situation where we can’t perform important operations because we didn’t think about it when we added the security checks, and now they get in the way. By excluding all security checks from the domain model, we can have a more flexible system because we can interact with the domain model in any way we want. The system is still secure because all clients are still required to go through application services. It is much easier to create new application services than to change the domain model.

Code example

These are two Java examples of what security enforcement might look like in an application service. This code has not been tested and should be considered more pseudocode than actual Java code.

Declarative security implementation

@Service
class MyApplicationService {

    @Secured("ROLE_BUSINESS_PROCESSOR") // 
    public MyBusinessProcess performSomeStuff(MyBusinessProcess input) {
        var customer = customerRepository.findById(input.getCustomerId()) // 
            .orElseThrow( () -> new CustomerNotFoundException(input.getCustomerId()));
        var someResult = myDomainService.performABusinessOperation(customer); // 
        customer = customerRepository.save(customer);
        return input.updateMyBusinessProcessWithResult(someResult); // }}Copy the code
  1. The annotation instructs the framework to only allow authenticated users of ROLE_BUSINESS_PROCESSOR with roles to invoke this method.
  2. The application service looks up the aggregation root from the repository in the domain model.
  3. The application service passes the aggregation root to the domain service in the domain model and stores the result (whatever the result is).
  4. The application service uses the results of the domain service to update the business process object and return it so that it can be passed to other application service methods participating in the same long-running process.

Manual safe Execution

@Service
class MyApplicationService {

    public MyBusinessProcess performSomeStuff(MyBusinessProcess input) {
        // We assume SecurityContext is a thread-local class that contains information
        // about the current user.
        if(! SecurityContext.isLoggedOn()) {// 
            throw new AuthenticationException("No user logged on");
        }
        if(! SecurityContext.holdsRole("ROLE_BUSINESS_PROCESSOR")) { // 
            throw new AccessDeniedException("Insufficient privileges");
        }

        var customer = customerRepository.findById(input.getCustomerId())
            .orElseThrow( () -> new CustomerNotFoundException(input.getCustomerId()));
        var someResult = myDomainService.performABusinessOperation(customer);
        customer = customerRepository.save(customer);
        returninput.updateMyBusinessProcessWithResult(someResult); }}Copy the code
  1. In a real application, you might create helper methods that throw an exception if the user is not logged in. In this example, I just include a more detailed version to show what needs to be checked.
  2. As in the previous case, only users with roles ROLE_BUSINESS_PROCESSOR are allowed to call this method.

Transaction management

Each application service method should be designed in such a way that it forms its own individual transaction, regardless of whether the underlying data store uses transactions. If an application service method succeeds, it cannot be undone unless an application service of another reversible operation is explicitly called (if the method even exists).

If you find yourself wanting to invoke more than one application service method in the same transaction, check that the granularity of the application service is correct. Maybe some of the things your application service is doing should actually be in the domain service? You may also want to consider redesigning your system to use final consistency rather than strong consistency (see the previous article on tactical domain-driven design for more on this).

Technically, you can handle transactions manually within application service methods, or you can use declarative transactions provided by frameworks and platforms, such as Spring and Java EE.

Code Examples These are two Java examples of what transaction management looks like in an application service. This code has not been tested and should be considered more pseudocode than actual Java code.

Declarative transaction management

@Service
class UserAdministrationService {

    @Transactional // 
    public void resetPassword(UserId userId) {
        var user = userRepository.findByUserId(userId); // 
        user.resetPassword(); // userRepository.save(user); }}Copy the code
  1. The framework ensures that the entire method runs in a single transaction. If an exception is thrown, the transaction is rolled back. Otherwise, the method is committed when it returns.
  2. The application service invokes the repository in the domain model to find the User aggregation root.
  3. The application service invokes business methods at the User aggregation root.

Manual transaction Management

@Service
class UserAdministrationService {

    @Transactional
    public void resetPassword(UserId userId) {
        var tx = transactionManager.begin(); // 
        try {
            var user = userRepository.findByUserId(userId);
            user.resetPassword();
            userRepository.save(user);
            tx.commit(); // 
        } catch (RuntimeException ex) {
            tx.rollback(); // 
            throwex; }}}Copy the code
  1. The transaction manager has been injected into the application service so that the service method can explicitly start a new transaction.
  2. If all goes well, the transaction will be committed after the password is reset.
  3. If an error occurs, the transaction is rolled back and the exception is rethrown.

choreography

Getting business processes right can be the most difficult part of well-designed application services. That’s because even if you think you’re just choreographing, you need to make sure that you don’t accidentally introduce business logic into your application services. So what does a business process mean in this context?

By choreography, I mean finding and calling the right domain objects in the right order, passing the right input parameters, and returning the right output. In its simplest form, an application service can look for an aggregation based on ID, invoke methods on that aggregation, save, and return. In more complex cases, however, the method might have to find multiple aggregations, interact with domain services, perform input validation, and so on. If you find yourself writing lengthy application service methods, ask yourself the following questions:

  • Make business decisions or require domain models to make decisions?
  • Should some code be moved to the domain event listener?

Having said that, it’s not the end of the world for some business logic to end up in the application services approach. It’s still very close to the domain model, and it’s packaged so well that it should be easy to refactor into a domain model later. Don’t waste too much time thinking about whether something should be put into a domain model or application service right away.

Code example

This is a Java example that shows what a typical business process looks like. This code has not been tested and should be considered more pseudocode than actual Java code.

A business process that involves multiple domain objects

@Service
class CustomerRegistrationService {

    @Transactional // 
    @PermitAll // 
    public Customer registerNewCustomer(CustomerRegistrationRequest request) {
        var violations = validator.validate(request); // 
        if (violations.size() > 0) {
            throw new InvalidCustomerRegistrationRequest(violations);
        }
        customerDuplicateLocator.checkForDuplicates(request); // 
        var customer = customerFactory.createNewCustomer(request); // 
        return customerRepository.save(customer); // }}Copy the code
  1. Application service methods run within a transaction.
  2. Application service methods can be accessed by any user.
  3. We call the JSR-303 validator to check that the incoming registration request contains all the necessary information. If the request is invalid, we throw an exception and report it to the user.
  4. We invoke a domain service that checks to see if a customer with the same information already exists in the database. In this case, the domain service will throw an exception (not shown here) that will be propagated back to the user.
  5. We invoke a domain factory that creates a new aggregation for Customer using the information from the registration request object.
  6. We invoke the domain repository to save the customer and return the newly created and saved customer aggregation root.

Domain event listeners

In our previous article on tactical domain-driven design, we discussed domain events and domain event listeners. However, we did not discuss the place of domain event listeners in the overall system architecture. As we recall from the previous article, domain event listeners should not affect the results of the method that originally published the event. In practice, this means that the domain event listener should run in its own transaction.

Therefore, I consider a domain event listener to be a special application service that is invoked not by a client but by a domain event. In other words: Domain event listeners belong to the application services layer, not the domain model. This also means that domain event listeners are coordinators that should not contain any business logic. Depending on what needs to happen when a domain event is published, you may have to create a separate domain service that determines how to handle the event if there are multiple forward paths.

That being said, in the section on aggregation in the previous article, I mentioned that it sometimes makes sense to change multiple aggregations within the same transaction, even if it violates the aggregation design guidelines. I also mentioned that this is best done through domain events. In this case, the domain event listener would have to participate in the current transaction, potentially influencing the outcome of the method of publishing the event, thereby breaking the design guidelines for domain events and application services. It’s not the end of the world as long as you do it intentionally and know the consequences. Sometimes you just need to be pragmatic.

Input and output

An important decision when designing an application service is deciding what data to consume (method parameters) and what data to return. You have three options:

  1. Use entity and value objects directly from the domain model.
  2. Use a separate data transfer object (DTO).
  3. Use domain payload objects (Dpos), which are a combination of the two above.

Each alternative has its pros and cons, so let’s take a closer look.

Entities and sets

In the first option, the application service returns the entire aggregation (or part of it). The client can do anything with them, and when changes need to be saved, the aggregation (or part of the aggregation) is passed back to the application service as a parameter.

This alternative works best when the domain model is anemic (that is, it contains only data and no business logic) and the aggregation is small and stable (because it is unlikely to change much in the near future).

It also works if the client is going to access the system through REST or SOAP, and the aggregation can be easily serialized to JSON or XML and returned. In this case, the client will not actually interact directly with your aggregation, but will instead use a JSON or XML representation of the aggregation that can be implemented in a completely different language. From the customer’s point of view, aggregation is just DTOS.

The advantages of this alternative approach are:

  • You can use classes that you already have
  • There is no need to convert between domain objects and Dtos.

The disadvantage is that:

  • It couples the domain model directly to the client. If the domain model changes, you must also change the client.
  • It imposes restrictions on how user input can be validated (more on this later).
  • You must design the aggregation in such a way that clients cannot put the aggregation in an inconsistent state or perform disallowed operations.
  • You may encounter problems with lazy loading of entities in aggregation (JPA).

Personally, I would try to avoid using this method.

Data transfer object

In the second option, the application service consumes and returns a data transfer object. Dtos can correspond to entities in the domain model, but more often they are designed for specific application services or even specific application service methods, such as request and response objects. The application service is then responsible for moving data back and forth between dtos and domain objects.

This alternative works best when the business logic in the domain model is very rich and the aggregation is complex, or when the domain model changes a lot while you want to keep the client API as stable as possible.

The advantages of this alternative approach are:

  • The client is decoupled from the domain model, making it easier to develop without changing the client.
  • Passing only the data that is actually needed between the client and application services improves performance (especially if the client and application services are communicating over a network in a distributed environment).
  • It becomes easier to control access to the domain model, especially if only certain users are allowed to invoke certain aggregation methods or view certain aggregation attribute values.
  • Only the application service will interact with the aggregation in the active transaction. This means that you can take advantage of lazy loading of entities in aggregation (JPA).
  • If dtos are interfaces instead of classes, you get more flexibility.

The disadvantage is that:

  • You get a new set of DTO classes to maintain.
  • You must move data back and forth between dtos and aggregations. This can be particularly tedious if dtos and entities are nearly identical in structure. If you work in a team, you need to be prepared with a good explanation of why dtos and aggregations must be kept separate.

Personally, this is the approach I started using most of the time. Sometimes I end up converting dtos to Dpos, which is the next alternative we’ll look at.

Domain payload objects

In the third option, the application service consumes and returns a domain payload object. A domain payload object is a data transfer object that knows the domain model and can contain domain objects. This is essentially a combination of the first two options.

This alternative works best when the domain model is poor, the aggregations are small and stable, and you are implementing operations involving multiple different aggregations. Personally, I would say I use the DPO more as an output object than an input object. However, IF possible, I try to limit the use of domain objects in the DPO to value objects.

The advantages of this alternative approach are:

  • You don’t need to create a DTO class for everything. You can do this when passing domain objects directly to the client is sufficient. You can create a custom DTO when you need one. When both are needed, they can be used together.

The disadvantage is that:

  • Same as the first choice. These disadvantages can be mitigated by including immutable value objects only in the DPO.

Code example

These are two Java examples using DTOS and DPOS, respectively. The DTO example demonstrates a use case where it makes sense to use a DTO instead of returning an entity directly: only a portion of the entity attributes are required, and we need to include information that does not exist in the entity. The DPO example demonstrates a use case where using a DPO makes sense: we need to include many different aggregations that are related to each other in some way.

This code has not been tested and should be considered more pseudocode than actual Java code.

Example data transfer object

public class CustomerListEntryDTO { // 
    private CustomerId id;
    private String name;
    private LocalDate lastInvoiceDate;

    // Getters and setters omitted
}

@Service
public class CustomerListingService {

    @Transactional
    public List<CustomerListEntryDTO> getCustomerList(a) {
        var customers = customerRepository.findAll(); // 
        var dtos = new ArrayList<CustomerListEntryDTO>();
        for (var customer : customers) {
            var lastInvoiceDate = invoiceService.findLastInvoiceDate(customer.getId()); // 
            dto = new CustomerListEntryDTO(); // 
            dto.setId(customer.getId());
            dto.setName(customer.getName());
            dto.setLastInvoiceDate(lastInvoiceDate);
            dtos.add(dto);
        }
        returndto; }}Copy the code
  1. A data transfer object is just a data structure without any business logic. This particular DTO is intended for the user interface list view, which only needs to display the customer name and the date of the last invoice.
  2. We look up all customer sets from the database. In a real application, this would be a paging query, returning only a subset of customers.
  3. The last invoice date was not stored in the customer entity, so we had to call the domain service to look it up for us.
  4. We create a DTO instance and populate it with data.

Example domain payload object

public class CustomerInvoiceMonthlySummaryDPO { // 
    private Customer customer;
    private YearMonth month;
    private Collection<Invoice> invoices;

    // Getters and setters omitted
}

@Service
public class CustomerInvoiceSummaryService {

    public CustomerInvoiceMontlySummaryDPO getMonthlySummary(CustomerId customerId, YearMonth month) {
        var customer = customerRepository.findById(customerId); // 
        var invoices = invoiceRepository.findByYearMonth(customerId, month); // 
        var dpo = new CustomerInvoiceMonthlySummaryDPO(); // 
        dpo.setCustomer(customer);
        dpo.setMonth(month);
        dpo.setInvoices(invoices);
        returndpo; }}Copy the code
  1. A domain payload object is a data structure without any business logic that contains domain objects (in this case entities) and other information (in this case years and months).
  2. We get the aggregation root of the customer from the repository.
  3. We obtain customer invoices for the specified year and month.
  4. We create a DPO instance and populate it with data.

Input validation

As mentioned earlier, aggregation must always be in a consistent state. This means, among other things, that we need to properly validate all inputs used to change the aggregation state. How and where do we do it?

From a user experience perspective, the user interface should include validation so that the user cannot even perform operations in the case of invalid data. However, relying on user interface validation alone is not good enough in a hexagon system. The reason for this is that the user interface is just one of many potential entry points in the system. If the REST endpoint allows any garbage into the domain model, the user interface will not validate the data properly.

When you think about input validation, there are actually two different kinds of validation: format validation and content validation. When validating a format, we check that certain values of certain types conform to certain rules. For example, social Security numbers are expected to follow a specific pattern. When we validate the content, we already have well-formed data and are interested in checking to see if that data makes sense. For example, we might want to check that a well-formed Social Security number actually corresponds to a real person. You can implement these validations in different ways, so let’s take a closer look.

Format validation

If you use a lot of value objects wrapped around primitive types (such as strings or integers) in your domain model (which IS my personal preference), it makes sense to build format validation directly into the value object constructor. In other words, if not to format the correct parameters, are not created for example EmailAddressorSocialSecurityNumber instance. There’s another advantage, if there is a variety of input data of the known methods, you can be in the constructor for some resolution and cleanup (for example, enter the phone number, some people may use Spaces or dashes Numbers can be divided into several groups, while others may use Spaces or dashes) don’t use any whitespace).

Now, when value objects are valid, how do we verify the entities that use them? Java developers have two options.

The first option is to add validation to the constructors, factories, and setup methods. The idea here is that it is not even possible to put the aggregation in an inconsistent state: all required fields must be filled in the constructor, the setters of any required fields will not accept empty parameters, and other setters will not accept incorrect value formats or lengths, etc. I personally prefer this approach when I use domain models with very rich business logic. It makes the domain model very robust, but it’s actually more or less impossible to bind properly to the UI, so it actually forces you to use Dtos between the client and application services.

The second option is to use Java Bean validation (JSR-303). Annotate all fields, and make sure your application service operates on them before running the aggregate Validator. I personally prefer to use this approach when working with the domain model of anemia. Even if the aggregation itself does not prevent anyone from putting it in an inconsistent state, you can safely assume that all the aggregations retrieved from the repository or validated are consistent.

You can also combine these two options by using the first option in the domain model and the Java Bean validation passed in to the DTO or DPO.

Content validation

The simplest case for content validation is to ensure that two or more interdependent properties in the same aggregation are valid (for example, if one property is set, the other must be NULL, and vice versa). You can implement this directly into the entity class itself, or you can validate constraints using Java beans at the class level. Because of the same mechanism, this type of content validation is provided free of charge when format validation is performed.

A more complex case of content validation is checking for the presence (or absence) of a value in a lookup list somewhere. This is the primary responsibility of the application service. Application services should perform lookups and throw exceptions as needed before allowing any business or persistence operations to continue. This is not something you want to put in entities, because these entities are mobile domain objects, and the objects needed to find them are usually static (see the previous article on tactical DDD for more information on mobile and static objects).

The most complex case of content validation is validating an entire collection against a set of business rules. In this case, the responsibilities will be divided between the domain model and the application services. The domain service will be responsible for performing the validation itself, but the application service will be responsible for invoking the domain service.

Code example

Here, we’ll look at three different ways validation can be handled. In the first case, we will examine performing format validation in the constructor of the value object (phone number). In the second case, we will look at an entity with built-in validation so that it is not possible to put the object in an inconsistent state in the first place. In the third and final case, we will look at the same entity, but do so using JSR-303 validation. This allows you to put an object in an inconsistent state, but not to save it to the database.

A value object with format validation

public class PhoneNumber implements ValueObject {
    private final String phoneNumber;

    public PhoneNumber(String phoneNumber) {
        Objects.requireNonNull(phoneNumber, "phoneNumber must not be null"); // 
        var sb = new StringBuilder();
        char ch;
        for (int i = 0; i < phoneNumber.length(); ++i) {
            ch = phoneNumber.charAt(i);
            if (Character.isDigit(ch)) { // 
                sb.append(ch);
            } else if(! Character.isWhitespace(ch) && ch ! ='('&& ch ! =') '&& ch ! =The '-'&& ch ! ='. ') { // 
                throw new IllegalArgument(phoneNumber + " is not valid"); }}if (sb.length() == 0) { // 
            throw new IllegalArgumentException("phoneNumber must not be empty");
        }
        this.phoneNumber = sb.toString();
    }

    @Override
    public String toString(a) {
        return phoneNumber;
    }

    // Equals and hashCode omitted
}
Copy the code
  1. First, we check that the input value is not NULL.
  2. The actual final phone number we store contains only numbers. For international phone numbers, we should also use the “+” as the first character, but we’ll leave that as an exercise for the reader.
  3. We allow, but ignore, Spaces and certain special characters that people often use in phone numbers.
  4. Finally, when all the cleaning is done, we check to see if the phone number is empty.

An entity with built-in validation

public class Customer implements Entity {

    // Fields omitted

    public Customer(CustomerNo customerNo, String name, PostalAddress address) {
        setCustomerNo(customerNo); // 
        setName(name);
        setPostalAddress(address);
    }

    public setCustomerNo(CustomerNo customerNo) {
        this.customerNo = Objects.requireNonNull(customerNo, "customerNo must not be null");
    }

    public setName(String name) {
        Objects.requireNonNull(nanme, "name must not be null");
        if (name.length() < 1 || name.length > 50) { // 
            throw new IllegalArgumentException("Name must be between 1 and 50 characters");
        }
        this.name = name;
    }

    public setAddress(PostalAddress address) {
        this.address = Objects.requireNonNull(address, "address must not be null"); }}Copy the code
  1. We call the setter from the constructor to perform the validation implemented in the setter method. If a subclass decides to override any method, there is little risk of calling a rewritable method from the constructor. In this case, it is best to mark setter methods as final, but some persistence frameworks may have problems with this. You just need to know what you’re doing.

  2. Here, we check the length of the string. The lower limit is a business requirement because each customer must have a name. Superordinate is a requirement of the database, because in this case, the database has a schema that allows only 50-character strings to be stored. By adding validation here, you can avoid annoying SQL errors later when you try to insert too long a string into the database.

An entity with JSR-303 validation

public class Customer implements Entity {

    @NotNull 
    private CustomerNo customerNo;

    @NotBlank 
    @Size(max = 50) 
    private String name;

    @NotNull
    private PostalAddress address;

    // Setters omitted
}
Copy the code
  1. This annotation ensures that the customer number cannot be empty when the entity is saved.
  2. This annotation ensures that the name cannot be empty or null when the entity is saved.
  3. This annotation ensures that the entity name cannot exceed 50 characters when it is saved.

Does size matter?

Before I move on to ports and adapters, let me briefly mention one more thing. Like all facades, there is a growing risk that application services will grow into giant god classes that know too much and do too much. These types of classes are often too large to read and maintain.

So how do you keep your application services small? The first step, of course, is to break up services that are too big into smaller ones. But there are risks. I’ve seen the similarities between the two services so much that developers don’t know what the difference is or which approach should be used for which service. The result is that service methods are scattered over two separate service classes, sometimes even implemented twice (once per service), but executed by different developers.

When designing application services, I try to make them as consistent as possible. In CRUD applications, this might mean that each aggregation provides only one application service. In more domain-driven applications, this might mean one application service per business process, or even a separate service for a specific use case or user interface view.

Naming is a good guide when designing application services. Try naming your application services based on the name of the application services rather than the aggregation they are concerned with. EmployeeCrudService or EmploymentContractTerminationUsecase better than EmployeeService its meanings, for example, the name of the much better. Also take a moment to think about your naming convention: Do you really need to end all services with the Service suffix? Will it make more sense to use suffixes such as Usecase or Orchestrator or even leave suffixes altogether in some cases?

Finally, I just want to mention command-based application services. In this case, you can model each application service model as a command object with a corresponding command handler. This means that each application service contains only one method that handles a command. You can use polymorphism to create specialized commands or command handlers. This approach results in a large number of small classes and is particularly suitable for applications where the user interface is command-driven in nature, or where the client interacts with application services through some messaging mechanism, such as message queuing (MQ) or enterprise service Bus (ESB).

Code example

I’m not going to give you an example of what the clergy looked like, because that would take up too much space. Also, I think most developers who have been in the industry for a while have seen their fair share of such courses. Instead, we’ll look at an example of a command-based application service. This code has not been tested and should be considered more pseudocode than actual Java code.

Command-based application services

public interface Command<R> { // 
}

public interface CommandHandler<C extends Command<R>, R> { // 

    R handleCommand(C command);
}

public class CommandGateway { // 

    // Fields omitted

    public <C extends Command<R>, R> R handleCommand(C command) {
        var handler = commandHandlers.findHandlerFor(command)
            .orElseThrow(() -> new IllegalStateException("No command handler found"));
        returnhandler.handleCommand(command); }}public class CreateCustomerCommand implements Command<Customer> { // 
    private final String name;
    private final PostalAddress address;
    private final PhoneNumber phone;
    private final EmailAddress email;

    // Constructor and getters omitted
}

public class CreateCustomerCommandHandler implements CommandHandler<CreateCustomerCommand.Customer> { // 

    @Override
    @Transactional
    public Customer handleCommand(CreateCustomerCommand command) {
        var customer = new Customer();
        customer.setName(command.getName());
        customer.setAddress(command.getAddress());
        customer.setPhone(command.getPhone());
        customer.setEmail(command.getEmail());
        returncustomerRepository.save(customer); }}Copy the code
  1. The Command interface simply marks the interface; it also indicates the result (output) of the Command. If there is no output from the command, the result can be Void.
  2. The CommandHandler interface is implemented by classes that know how to process (execute) a particular command and return the result.
  3. The client interacts with CommandGateway to avoid having to look up individual command handlers. The gateway knows all the command handlers available and how to find the right one for any given command. The code for the lookup handler is not included in the example because it depends on the underlying mechanism for registering the handler.
  4. Each Command implements the Command interface and includes all the necessary information to execute the Command. I like to use built-in validation to make commands immutable, but you can also make them mutable and use JSR-303 validation. You can even leave commands as interfaces and let the client implement them for maximum flexibility.
  5. Each command has its own handler, which executes the command and returns the result.

Ports and adapters

So far, we have discussed the domain model and the application services that surround and interact with it. However, if the client cannot invoke them, which is where the ports and adapters are located in the picture, these application services will be completely useless.

What is a port?

A port is an interface between a system and the outside world that has been designed for a specific purpose or protocol. Ports are used not only to allow external clients to access the system, but also to allow the system to access external systems.

Now, it’s easy to start thinking of ports as network ports and protocols as network protocols (such as HTTP). I made this mistake myself, and in fact, Vernon does it in at least one example in his book. However, if you take a closer look at Alistair Cockburn’s article to which Vernon refers, you will see that this is not the case. Actually, it’s a lot more interesting than that.

A port is a technically agnostic application programming interface (API) that has been designed for specific types of interactions with applications (hence the name “protocol”). How you define this protocol is entirely up to you, which is what makes this approach exciting. Here are some examples of the different ports you might have:

  • The port that the application uses to access the database
  • The port that your application uses to send messages such as E-mail or SMS
  • The port that human users use to access your application
  • Ports that other systems use to access your application
  • The port that a specific user group uses to access your application
  • A port that exposes a particular use case
  • A port designed specifically for polling clients
  • A port designed for subscription customers
  • A port designed for synchronous communication
  • A port designed for asynchronous communication
  • A port designed for a specific type of device

This list is by no means exhaustive, and I’m sure you can come up with more examples yourself. You can also combine these types. For example, you might have a port that allows an administrator to manage users using clients that communicate asynchronously. You can add as many ports to the system as you need without affecting other ports or the domain model.

Let’s look at the hexagonal structure again

Each side of the inner hexagon represents a port. This is why this architecture is often expressed this way: you have six sides at once, available for different ports, and enough room to plug in as many adapters as you need. But what is an adapter?

What is an adapter?

As I mentioned, ports are not about technology. However, you can still interact with the system through certain technologies -Web browsers, mobile devices, dedicated hardware devices, desktop clients, and so on. This is where the adapter comes in.

Adapters allow interaction through specific ports using specific technologies. Such as:

  • REST adapters allow REST clients to interact with the system through certain ports
  • The RabbitMQ adapter allows the RabbitMQ client to interact with the system through certain ports
  • The SQL adapter allows the system to interact with the database through a port
  • Vaadin adapters allow human users to interact with the system through certain ports

You can use multiple adapters for a single port, or even a single adapter for multiple ports. You can add as many adapters to the system as you need without affecting other adapters, ports, or domain models.

Ports and adapters in the code

By now, you should have some conceptual understanding of ports and adapters. But how do you translate these concepts into code? Let’s see!

In most cases, ports will be specified as interfaces in code. For the ports that allow external systems to access your application, these are your application service interfaces:

The implementation of the interface resides within the application services layer, and the adapter uses the service only through its interface. This is very consistent with the classic layered architecture, where the adapter is just another client that uses your application layer. The main difference is that the concept of ports helps you design better application interfaces, because you actually have to think about what the client of the interface is, and realize that different clients may need different interfaces, rather than a one-size-fits-all approach.

Things get even more interesting when we look at the ports that allow your application to access external systems through certain adapters:

In this case, the adapter implements the interface. The application service then interacts with the adapter through this interface. The interface itself is located in your application service layer (for example, factory interface) or domain model (for example, repository interface). This approach is not allowed in traditional layered architectures, because interfaces will be declared at the upper level (” application layer “or” domain layer “) but implemented at the lower level (” infrastructure layer “).

Notice that in both of these approaches, the dependency arrows point to the interface. The application is always decoupled from the adapter, and the adapter is always decoupled from the application implementation.

To make this more concrete, let’s look at some code examples.

Example 1: REST API

In the first example, we will create a REST API for our Java application:

Ports are application services that are suitable for exposure through REST. The REST controller acts as an adapter. Naturally, we use frameworks such as Spring or JAX-RS, which provide both servlets and off-the-shelf POJOs (plain Java objects) and XML/JSON mapping. We just need to implement the REST controller:

  1. Take raw XML/JSON or deserialized POJO as input
  2. Invoke the application service
  3. The response is constructed as raw XML/JSON or POJO to be serialized by the framework, and then
  4. The response is returned to the client.

Clients, either client-side Web applications running in browsers or other systems running on their own servers, are not part of this particular hexagon system. Nor does the system care who the clients are, as long as they conform to the protocols and technologies supported by the ports and adapters.

Example 2: Server-side Vaadin UI

In the second example, we’ll look at another type of adapter, the server-side Vaadin UI:

Ports are application services suitable for exposure through a Web UI. The adapter is a Vaadin UI that converts incoming user actions into application service method calls and the output into HTML that can be rendered in the browser. Thinking of the user interface as just another adapter is an excellent way to keep business logic outside the user interface.

Example 3: Communicating with a relational database

In the third example, we’ll turn around and examine an adapter that allows our system to call out to external systems, more specifically relational databases:

This time, because we’re using Spring Data, the port is the repository interface in the domain model (if we’re not using Spring Data, it might be some kind of database gateway interface that provides access to the repository implementation, transaction management, and so on).

The adapter is Spring Data JPA, so we don’t really need to write it ourselves, just set it up properly. When the application starts, it automatically implements the interface using a proxy. The Spring container is responsible for injecting the proxy into the application services that use it.

Example 4: Communicating with external systems through REST

In the fourth and final example, we’ll look at an adapter that allows our system to call out to external systems via REST:

Because the application service needs to communicate with external systems, it is declared to be used for this interface. You can think of it as the first part of the anti-corruption layer (if you need to learn more about DDD, go back to the article on strategic DDD).

The adapter then implements this interface, forming the second part of the corruption layer. As in the previous example, the adapter is injected into the application service using some kind of dependency injection, such as Spring. It then makes calls to external systems using some internal HTTP clients and converts the received responses into domain objects specified by the integration interface.

Multiple bounded contexts

So far, we have only examined the appearance of a hexagon architecture when applied to a single bounded context. But what happens when you have multiple finite contexts that need to communicate with each other?

If the context is running on a separate system and communicating over a network, you can create a REST server adapter for the upstream system and a REST client adapter for the downstream system:

The mapping between different contexts takes place in the adapter of the downstream system. If the context runs as a module in a single overall system, a similar architecture can still be used, but only one adapter is required:

Since both contexts are running in the same virtual machine, we only need an adapter that interacts directly with both contexts. The adapter implements the port interface of the downstream context and invokes the port of the upstream context. Any context mapping takes place inside the adapter.