The Domain Driven Design’s Missing Pattern

Carmine Ingaldi
Sep 4, 2020 · 8 min read

From its birth in 2003, Domain Driven Design doctrine has, in some way, changed the way we look at software architecture. This is even more true at the verge of the 20s, when microservices are a serious thing and we all have rediscovered the importance of Eric Evans theory and his “strategic patterns”. Most enterprise frameworks are based on DDD concepts, but only few of them consider what happens in the domain layer since, by definition, that is the territory of domain experts and very few patterns could be enforced (unless you are into CQRS/ES!)

Almost each of the DDD implementations start from a Service. But “Services” are a very broad concept in computer engineering, there are many kind of services as many roles a generic component could have in an app architecture. Most of times though, we build layered architectures where an Application Service segregates domain logic, which soon becomes a Transaction Script that implements behaviours for anaemic domain models. To prevent this, I’ve found that is important to implement an often overlooked object but originally discussed from Eric Evans: the Domain Service, which is able to separate domain logic from application logic when it’s not clear where some operation fits better

So what are Domain Services?

A service is an operation offered as an interface that stands alone in the model, without encapsulating state, as entities and value objects do. Services are a common pattern in technical frameworks, but they can also apply in the domain layer.

(Eric Evans 2003 , DDD Blue Book)

If you feel that things are not more clear after reading the official definition, I can tell you how I personally see them: Domain Services are domain objects that don’t encapsulate data, just expose behaviours. But let’s go deeper

Domain Logic vs. Application Logic

To have a clue of the subject we should try to address this thorny issue.

This how a DDD Layered (onion-ed, in this representation) Architecture looks like, at its core

Here you can recognize that two components play a key role: the Application Service that exposes API to execute entire/parts of business workflows (Uncle Bob would call them Use Cases) and Aggregate Root that acts as a façade towards the aggregate. Therefore, their partnership defines the Business Logic

So, which part of the logic belongs to which layer is always a debated subject, but two remarks can help us to make the right decision

  • In layered/exagonal/onion/whateverNameItsAlmostTheSame architectures, the upper layers implement policies and the lower layers implement rules. In brief, we go from “What to do?” to “How to do it?”
  • Domain Logic is, precisely, Domain. It’s distilled from domain knowledge. It’s what a domain expert usually does by hand before you implement a software to replace him/her. If the answer to the question “Should I have domain knowledge to understand this code?” is “Yes”, than you know where to put it

In case of Doubt…

Now it looks clear why Anaemic Domains harm our projects. They mix the “what” and the “how”, violate the most traditional concepts of decoupling and separation of responsibilities, so the software is difficult to understand, maintain and -worse- extend. A good indicator of how much rich a domain is, is to observe how application logic looks like. In my ideal world every “*Service” object method, if used to execute commands, should just

  1. load an aggregate root
  2. call a method on the aggregate root
  3. save the aggregate root

Too bad, this always remains in my dreams. Sometimes I find out that some domain logic is not aggregate logic in sense that does not fit with aggregate responsibility to just check invariants (using its own data), mutate its internal state and, bonus, emit an event. The reason can be motivated by essential domain complexity or just that anaemic domains doesn’t permit to add aggregate behaviours easily.

Here domain services come into play. They follow the ubiquitous language, and resemble operations, so they are called “ClosestPoint”, “DocumentFormatting”, “BillCalculation”. As we said, they don’t hold state. This doesn’t mean that they have to be pure functions, being stateless doesn’t prevent to have dependencies from other objects, potentially for the sake of refactoring. Moreover, Domain Services could be just ports that have their implementations in the outer circles of the architecture…But no hurry! Let’s discuss about it with some code example

Where The Domain Service Fits?

Move Things With Aggregates

First rule of Aggregates is: You do not talk about Aggregates

Aggregates instances cannot collaborate with each other (including other instances of the same aggregate), and this will save us to think about concurrency problems and failure modes. In the real world, though, aggregates sometimes collaborate together. DDD addresses this aspect through domain events that can cause domain actions outside the triggering aggregate but, as we know, events imply async interaction patterns

There are situations where we want to avoid this: sometimes just for the sake of pragmatism that suggests us to not overengineer simple things, but sometimes instead, it’s matter to guarantee transactional behaviour to our business logic. Distributed* transactions in event driven applications are sagas and they’re an overkill if not needed to coreograph complex flows between remote systems through significant time spans.

Let’s look at an example where a banking application performs transfers between its own customer accounts in a transactional fashion**

So we choose just to load aggregates in a single use case executions and work with them…but where to put that “orchestration logic”? Here domain services come into play

Here the domain service hides business decisions and operations from the application logic, which is always good. Careful readers have already thought that some of the logic performed into domain service are about checking invariants, so they could belong to the aggregate logic. This should not be taken for granted as not every check is an aggregate responsibility, only those who are domain-relevant:

  • Checking account balance against transfer amount would be a withdraw() resposibilty
  • Checking that transfer amount is below the limit is not related to any account (maybe to a transfer VO? )

This will keep aggregates as small as possible and, the rest would belong to Domain Services

*don’t forget that even monolithic applications are distributed systems: they communicate with databases!
**Some technologies, such as Spring Framework, permit transactional behaviour even in interactions driven by local events. Since this is a technical detail, I’m not taking it into account for this example. Moreover, there are some pitfalls in that approach that should lead to a careful implementation

Create Strategies for Aggregates

As already mentioned, we separate domain logic from application logic using the following rule of thumb: What to do or How to do it? In some cases the line is more blurred than in the others, especially when there are multiple ways to do it and the decision on which strategy to choose is based on information that come from elsewhere (in the next paragraph we will go deeper into it). In e-commerce applications, for instance, when customers do checkout, discount campaigns would be applied on products if ever some rules were matched

Here we are doing odd things in application logic because we need to make a decision. This decision could be made directly from the aggregate (in checkout() method body) but this leads your user to know about the calendar and, worse, ties up you core domain logic to policies that change frequently and in unpredictable ways. The reasoning is correct, the implementation could get better:

In this implementation, application logic still decides what to do (check if some kind of discount can be applied and than complete the purchase, which is the same thing a casher would do in a physical shop!). The how to do it part now belongs completely to domain logic but we got both things achieving a good extensibility and clearly separating responsibilities.

Two remarks:

  • in FixedRateDiscountStrategy I’m passing the rate as a property, It wasn’t really necessary but I’ve done it to create confusion! Now that the object holds data, is it still a Domain Service? The answer is no. That now is a Value Object. To be more precise, it’s what Eric Evans defines as a “Specification”. The difference here is subtle again, but the most important thing is that we can use Domain Services to define different behaviours, apply rules outside the aggregate boundary and then make it evolve to something else, if needed
  • Making a Domain Service instance pass through aggregate boarders it’s not very good in general, since it creates coupling. But here the aggregate just knows an interface contract, so it’s ok even if we can see domain services as a thiny upper sublayer in the domain side

Observe External Status

Many scenarios cannot be executed just relying on information that can be retrieved from the aggregate. Interactions with other Bounded Contexts are unavoidable. Domain Services can act as abstractions of external services

Here we have slightly changed out checkout use case implementation, ’cause product management has decided to introduce promocodes to distribute discounts. I suppose that promocodes are a concept that belongs to marketing function, so they don’t have a meaning in the user purchase BC. Nevertheless, I still need it, so promocodes are entering, in some extent, into my domain, but still doesn’t belong to any aggregate, nor we want to hydrate Promocodes in our BC listening to events. So this is a Domain Service job.

As every services, they could follow the “port and adapters” pattern: they don’t have to hold a state, so Domain Services could be offered as an interface (port) in the domain layer and be implemented (adapter) at infrastructure level, to implement communication with external system (roughly speaking, the HTTP call to Promotions-service) . And what about monolithic apps? A simple approach here could be the following

package com.myecommerce.promotions_srv.promocode.applicationpublic class PromocodeService extends PromocodeInfoProxy {

@Override
public PromocodeInfo getInfo(String promocode) {
....

Very ugly but still pragmatic. A step behind writing and inter-BC adapter that will be replaced in few minutes with an HTTP call, making microservices splitting easier

Refactoring an Anaemic Domain?

Almost everything in the examples that we have seen could be done in a different way, ofter this way is more DDD-friendly. Aggregates should not be mutated in the same Unit of Work, many calculations find place directly in entity methods (or in projections, if we switch to CQRS) and finally, if you need to combine data from multiple aggregates, even in different BCs, you should ask yourself first if you are doing your design correctly

Yes, true…but quoting Alberto Brandolini

Software development is a learning process, working code is a side effect

So, if it’s true we should start focusing on learning and evolving our code. Refactoring just means coding new things we have learnt from the last commit, and we do it step by step, gently

This is not what I mean for “refactoring”

The advantage of using Domain Services is to start to factor out domain logic, readabilty and testability will increase and lets you figure out how to enrich aggregate behaviour. In some extent, they act as an anticorruption layer

Conclusions

Most of the mid/large applications are a perfect field to explore with DDD approach. Doing DDD, though, doesn’t just mean blindly applying patterns and concepts. It’s more about the way you gain knowledge and how you use this knowledge to make your architecture evolve. Patterns and concepts are the way we achieve this goal, and Domain Services can play a crucial role here

The Startup

Get smarter at building your thing. Join The Startup’s +729K followers.