Domain-Application-Infrastructure Services pattern
As basic building blocks of DDD1, Domain Entities and Value Objects should contain most of the business logic of any application. However, there are some scenarios where Entities and Value Objects aren't the best solutions. Eric Evans said about this in his book2:
When a significant process or transformation in the domain is not a natural responsibility of an Entity or Value Object, add an operation to the model as standalone interface declared as a Service. Define the interface in terms of the language of the model and make sure the operation name is part of the Ubiquitous Language. Make the Service stateless.
So when there are operations that need to be represented, but Entities and Value Objects aren't the best place, you should consider modeling these operations as Services. In Domain-Driven Design, there are typically three different types of Services you'll encounter:
- Application Services: Operate on scalar types, transforming them into Domain types. A scalar type can be considered any type that's unknown to the Domain Model. This includes primitive types and types that don't belong to the Domain.
- Domain Services: Operate only on types belonging to the Domain. They contain meaningful concepts that can be found within the Ubiquitous Language. They hold operations that don't fit well into Value Objects or Entities.
- Infrastructure Services: Are operations that fulfill infrastructure concerns, such as sending emails and logging meaningful data. In terms of Hexagonal Architecture, they live outside the Domain boundary.
Sometimes referred to as "Workflow Services" or "User Cases", these services orchestrate the steps required to fulfill the commands imposed by the client. While these services shouldn't have "business logic" in them, they can certainly carry out a number of steps required to fulfill an application need. Typically, the Application Service layer will make calls to the Infrastructure Services, Domain Services, and Domain Entities in order to get the job done.
Application Service objects are "command" objects. That is, in relation to the command/query separation, you wouldn't talk to an Application Service simply to retrieve information. Instead, you'd go directly to the Domain Services for such information.
Since the Application Service orchestrates application workflow, it would make no sense for them to be called by the Domain Service (or even by themselves) - a workflow has only one single starting point; if an Application Service could be called by other entities within the domain model, it would imply that a workflow has an indeterminate number of starting points.
Domain Service should be directly related to the core business functionality. A common example is assigning invoice numbers. It could be a responsibility of the Invoice itself to generate these numbers in series or according to set rules, taking the client’s identifier, date, etc. into account. It may, however, strike some people as odd. And such a decision could lead to trouble in any more complex cases; for example, if invoice numbers are generated differently between clients or require access to some external state. If this is true then the responsibility may belong to a domain service. Such domain services can be consumed by an aggregate root or invoked by an Application Service which only passes the result to the domain.
More technical kinds of Domain Services are Factories3 and Repositories4. They are part of the domain (the interfaces at least). Less obvious, in the case of repositories, it is clear that factories very much belong there. Factories, used to create Aggregate Roots and/or Entities, require knowledge obtained from the Domain Expert and operate based on business decisions. Consider a hypothetical VehicleFactory which would create instances of Car or Truck entities depending on the characteristics of that vehicle (size, mass, etc.).
Lastly, a Domain Service is often used whenever a given operation must access more than one Aggregate Root. Many would say that each operation should update only a single Aggregate Root but it may not always be possible. Especially in the face of requirement changes to an existing application. If ever required, a Domain Service would be used rather than having Aggregate Roots access each other.
These are services that typically talk to external resources and are not part of the primary problem domain. The common examples are emailing and logging. When trying to categorize an Infrastructure service, we can ask ourselves the following questions:
- If we remove this service, will it affect the execution of my domain model ?
- If we remove this service, will it affect the execution of my application ?
If it will affect our domain model, then it's probably a "Domain Service." If, however, it will simply affect our application, then it is probably an Infrastructure Service. So for example, if we take off Email Notifications from an application, it probably wouldn't affect the core domain model of the application; it would, however, completely break the usability of the application - hence it is an Infrastructure service (and not a Domain Service).
The Infrastructure service can only be invoked by the Application Service. Given the above questions, this makes sense. If the Domain Services or the Domain Entities could invoke the Infrastructure Services (think Logging, think Email confirmations), then we would probably have application logic leaking into our domain model. Plus, since logging and emailing are typically part of some greater "workflow," it makes sense that they'd be called from the "Workflow Services" (ie. Application Services).
Anemic Domain Models Vs Rich Domain Models
We must be cautious not to overuse Domain Service abstractions within our system. Following this path can lead to Entities and Value Objects being stripped of all behavior and becoming mere data containers. This is contrary to the goal of Object-Oriented Programming, which can be thought of as the gathering of both data and behavior into semantic units called objects, with the intent of expressing real-world concepts and problems. Domain Service overuse can be considered an anti-pattern and is referred to as the Anemic Domain Model.
Typically, when starting a new project or feature, it's easy to fall into the trap of modeling the data first. This commonly includes thinking that each database table has a direct one-to-one object form representation. However, this thinking may or may not be the exact case all the time.
Anemic Domain Model Breaks Encapsulation
If we revisit the code used to define the services within our Service layer, we can see that as a client making use of the Order Entity, we're required to know every detail of its internal representation. This finding goes against the fundamental rule of Object-Oriented Programming, which is combining data with subsequent behavior.
Anemic Domain Model Brings a False Sense of Code Reuse
Say there's an instance where a client bypasses UpdateOrderAmountService and instead fetches, updates, and persists directly to OrderRepository. Then, all the extra business logic that the UpdateOrderAmountService service might have won't be executed. This could lead to the order being stored in an inconsistent state. As such, invariants should be correctly guarded, and the best way to do this is to let the true Domain Model handle it. In the case of this example, the Order Entity would be the best place to ensure this. Note that by pushing this action down into the Entity and naming it in terms of the Ubiquitous Language, the system achieves great code reuse. Anyone who now wishes to change the amount of the order has to invoke the Order.changeAmount method directly. This leads to far richer classes, where behavior is the goal for code reuse. This is commonly referred to as a Rich Domain Model.
How to Avoid Anemic Domain Models
The way to avoid falling into an Anemic Domain Model is to, when starting a new project or feature, think of the behavior first. Databases, ORMs, and so on are just implementation details, and we should strive to push the decision to use these tools as late in the development process as we can. In doing so, we can focus on the one true attribute that matters: the behavior.
1. “Domain-driven Design”, This content ↩
2. “Domain-Driven Design: Tackling Complexity in the Heart of Software”, Eric Even, https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215 ↩
3. “Factory pattern”, This content ↩
4. “Repository pattern”, This content ↩