Adding Domain-Driven Design to Ports & Adapters - Enriching the Domain Model with Logic
The current codebase, through enriched with stronger types, still is mostly procedural. Let's see how we can combine data and logic to build a much richer domain model.
In the previous article, I demonstrated what types of data structures exist and which of those can contain logic. It turns out that most data structures used in various concerns are actually DTOs that are best left devoid of any logic. Only the domain model, which can be placed at the core of the hexagon, should contain the biggest part of the business logic.
In this article, I want to illustrate how to find logic in a codebase and move it to the domain model.

Identifying Logic for the Domain Model
Currently, all the domain model classes contain no logic at all and are therefore anaemic data classes, a code smell in OOP.
An indication for displaced logic that should be moved into the domain model can be found when we investigate which classes access the getters from that anaemic domain model to perform some logic like validation, formatting, or more complex manipulations on that data.
Depending on the context and concern these classes are defined in, such direct data access can be considered a code smell called Feature Envy, meaning that a class is envious of the features (e.g. data, state) another class provides. This access creates a tight coupling between the two classes.
To increase cohesion and reduce coupling, the logic applied on that data can often be isolated in an own method and moved to the accessed class, as shown below.

In procedural code, we often first need to untangle some cohesive logic and extract it into a separate method before moving it from the envious to the accessed class.
What we can typically see as soon as we extract a method containing the accessed class is that the accessed class is part of the parameter list of this method.
Such parameter objects are possible targets to move the method to, especially if there is only one parameter.
Honour the Concern- and Dependency Rules!
Before we move methods into other classes however, we need to be careful not to break concern- and dependency rules.
Logic should mainly be moved around within a concern. As soon as we cross concern boundaries, we risk that logic ends up in the wrong place.
For example, presentation logic should not all of a sudden end up in the business concern just because the presentation controller adapter accesses many fields of an application-concern data structure or domain model to create its output JSON.
The same is true about moving translation logic from the data-access concern into the hexagon’s core. The domain class should not know how it is translated to a DB entity and thus, this logic should remain within the data-access adapter.
For our purpose of creating a rich domain model, this means that logic can only be moved from the application concern into the domain model.

Example Refactoring
Validation Logic in the Service
Let’s have a look at the following code snippet taken from the codebase:
We see a lot of logic to ensure that the ReservationPeriod is in a valid state before we can process the reservation request any further.
This piece of logic does not require and additional call to another system and can be done completely in isolation. All it does is accessing the ReservationPeriod’s startTime and endTime. It is therefore a great candidate to be moved into the actual ReservationPeriod class.
In fact, we could restructure the code a bit to validate startTime and endTime when we create the ReservationPeriod. The following video illustrates this refactoring.
Start by temporarily extracting startTime and endTime into separate local variables so that the code block does not contain any strong type anymore.
Wrap the constructor of ReservationPeriod into a static creation method such that we can only create a new ReservationPeriod through that method.
Copy-paste the logic into that creation method (together with any constants defined in the service class that are only accessed by that logic).
Remove the logic from the service method.
We can further improve the creation methods readability by solving the Complicated Boolean Expression code smells through the introduction of two intention-revealing static query methods, shorterThanMinimalReservationDuration (and rename the corresponding exception), and outsideOperatingHours, which allows us to remove the one-line redundant and obsolete comment code smells that were present before:

Trade-Offs of Domain Model Classes in Port Interfaces
However, now we have a bit of am issue that we need to take care of: we need to mitigate the risk of exceptions being thrown outside the try-catch block in the calling controller class:

This is one of the trade-offs of exposing complex domain model classes in port interfaces. Even though we can spare one translation step by avoiding an additional application-specific port DTO to map to and from, we now need to take care of the correct error handling outside the hexagon.
We could also have saved one translation step if we used the presentation concern’s request DTO inside the ForReservingParkingSpots port, but then we would again have had the application concern depend on the presentation concern, which we want to avoid for several reasons discussed in earlier articles.
Alternatively, we could think about simply passing the three parameters startTime, endTime, and reservingMember as primitive types into the port again and moving the creation of ReservationDetails into the port-implementing service. Again, this leads to the Data Clump code smell described in an earlier article.
Conclusively, we need to honour the dependency rules and at the same time avoid excessive translation between data structures containing exactly the same fields.
In this example, we can mitigate the exception-handling issues by introducing an additional creation method on the ReservationDetails class that only takes primitive data types and returns the final ReservationDetails object.
This hides away the details of how it is created and reduces the additional potential exception handling cases to one line:
The video shows the refactoring:
Increasing Expressiveness of the Domain Model
An additional refactoring that may seem an overkill initially is to wrap the Reservation class’s constructor into a more domain-specific creation method.
In the real world, we could be talking about “scheduling a reservation”. This is information that needs to be inquired from the domain experts and is part of the ubiquitous language of that specific bounded context. A good name for that creation method could be schedule:

You can find the refactored codebase here.
Conclusion
In this article, I have demonstrated how feature envy can show us where logic may be displaced, and how we can build up a richer domain model by wrapping parts of that logic into own methods and moving them into these classes.
Furthermore, I showed how domain-specific creation methods help to hide away the complexity of how domain model classes are internally structured and created, further improving maintainability and providing a place to put validation logic.
I have also illustrated how each decision comes with trade-offs and need to be weighed against each other. Being proficient in the refactoring tools the IDE provides is therefore inevitable or else, the codebase may remain in suboptimal conditions.
The more we separate logic into smaller pieces and move them closer to the data they use, the clearer the entire domain becomes. This can also lead to breakthroughs in understanding, something not possible if we keep the code in a procedural or technically convoluted state.
Now that the domain model already contains some logic, I will explore how to move from Hexagonal to Clean Architecture. Subscribe if you don’t want to miss it:
Resources
This article is part of a series moving towards a business-centric architectural design. See the following post to get started.
This series of articles is an addition to the From Layered to Hexagonal Architecture in 2 steps article.
We touch on the same topics in our O’Reilly course “DDD, EventStorming, and Clean Architecture”. Check it out to learn how to use Classic TDD to implement DDD Aggregates in Hexagonal and Clean Architecture.
If you want to learn more about effectively separating concerns, you could also have a look at our on-premise, remote or hybrid workshop Modern Software Architecture Design Patterns.
To effectively learn how to identify code smells and refactor them, check out our workshop on Clean Code.