Greetings, Domain Service. You have been recruited by the Business Logic to defend the frontier against Inconsistency and the Interfaces armada.
Few days ago, I wrote about the Hexagonal Architecture, explaining why you should use this powerful architectural pattern to design your system.
In this article I want to deep dive in one of the most important parts of the Hexagonal Architecture: the Domain Services.
It depends from which point of view you describe it.
From a client perspective, a Service is the only endpoint available to interact with the business logic.
From an internal perspective, a Service coordinates and executes the business logic while guarding your business invariants.
Pretty abstract, isn’t it? Let’s go into more details.
Everything in a Service happens sequentially.
When an Interface calls one of the methods available on the Service surface, this is the typical sequence executed inside:
Let’s go through the steps one by one.
Being the point of contact between business logic and Interfaces, the Service must translate incoming requests to something meaningful to the internal domain.
This basically means transforming method parameters into Value Objects belonging to the domain.
This is not only a mere data representation transformation, though, but also an early application of your domain constraints. Usually developers refer to it as validation.
Let’s see an example.
Imagine you’re developing a very simple Bed&Breakfast Management System.
One of the modules must allow a generic Admin to add new B&Bs by their Name.
The first business constraint that you want to enforce is that the B&B name cannot be empty.
And you want to enforce it in the translation phase, when you wrap the incoming string value in a meaningful Name value object.
The idea is that an empty Name doesn’t make sense in your domain, so it shouldn’t be even possible to construct such an object.
This is how your Value Object declaration would look like:
Let’s also assume that your system handles many properties from different Owners. This means that in order to create a new B&B for a specific Owner, we need to pass the OwnerId along with the B&B Name.
This is how your Service endpoint looks so far:
Let’s add a couple more rules to your fictional domain:
#2 means that before adding a new B&B, you must check whether an Owner is activated.
You have found another business constraint. But this time it’s different.
This time your constraint is not bound to the B&B. This time, your constraint depends on an external Entity (the Owner) which is not the subject of your Service.
Therefore, the relevant question here is: where should we define this new constraint?
In the B&B Entity class? Or in a Value Object semantically belonging to the Entity, as we did for the Name?
The answer is: neither. The only place for checking cross-Entity constraints is the Service itself.
The subject Entity hasn’t access to any knowledge about Owners and their current state. This is why this constraint can’t belong to the B&B Entity.
The Service has to handle it in order to keep the overall state consistent with the business invariants.
Let’s update the code and add the constraint at Service level:
It’s time to take care of the B&B itself.
After passing all cross-Entity constraints, the Service can safely manipulate the subject Entity.
In this case, you want to create a new B&B belonging to the activated Owner and save it.
Together with the cross-Entity constraints, this is the very core of our business logic.
Here’s how it goes:
In a following use case you may want to, e.g., add a Room to an existing B&B.
This is how the updated Service would look like in that case:
In the context of the Hexagonal Architecture, a Service is your implementation of the write side of a CQRS architecture.
Therefore it must not have any get* method available to retrieve information on demand.
This responsibility is fully on one of the Read Models. And Read Models, in their role of Decision Support Systems, do not belong to any Service. They belong to the Interfaces.
Then how do you inform the outer world that business logic was successfully applied and something relevant happened?
Simple: you publish a Domain Event.
A Domain Event is the only information that escapes the black box of a Service (with one exception, see later in this section).
A B&B was successfully added?
The Service publishes a BnBWasAdded event.
A new Room was added to a specific B&B?
The Service publishes a RoomWasAdded event.
All external systems waiting for those events to be published to trigger their own behavior must subscribe to them in a Publish/Subscribe fashion.
How you technically publish events is something specific to your system and your needs. I find the combination of a LoopBack (in memory) publisher and a Message Broker one (e.g. Kafka) usually a good solution.
But the only thing that the Service needs in order to publish an event is a Publisher interface. So let’s add it to our code.
A couple of things happened in this last code update, apart from adding the Publisher interface.
#1 — a ReadOnlyBnb interface was introduced and implemented by the Bnb Entity.
This is useful when you want to share information about your Entity but you don’t want to expose the functionality that changes its internal state.
It also serves the purpose of providing information about the internal state to the Repository implementation. When the save method is called, all required information will be at hand for the Repository to store the Entity in whatever format you need.
# 2 — Now the Service publishes an event every time the business logic is successfully executed and the state of the system is changed.
How to serialize those events into messages is inherent to your system and the technologies you use, so I won’t go deep into it.
At the beginning of the section, I anticipated that a Domain Event is the only information that escapes the black box of a Service, with only one exception.
This exception is the return value of the methods of your Service.
Yes, your Service should actually return something.
This is critical for the Interfaces to have immediate feedback on the command they requested and to avoid useless, accidental complexity.
But what exactly should the Service return?
Not void. Not a boolean. Not null. Not your naked Entity.
A Result is a container object which carries information about the outcome of an operation inside your Service.
If the operation was successful, the Service returns a Result object which wraps the operation subject (e.g. the freshly added (ReadOnlyBnb) Bnb).
If the operation was not successful, the Service still returns a Result object. But in this case the object will wrap the exception(s) that occurred inside the Service method call.
If you are used to Functional Programming, this is something similar to the Either monad.
In no case a Service method should let an exception escape outside of its box.
You want to 100% control what your Service communicates back to the Interfaces. Let internal exceptions freely escape out of the Service goes against this principle.
This is a Java implementation of the Result micro-library which the code example uses. (Yes, it uses emojis as method names!)
Now let’s add the finishing touch to the Service by making it robust and failure-proof, and by returning a Result object. You may also want to add some logging at this point.
So, here’s how the Service uses the Result library to achieve elegance and robustness at the same time:
This is a nice way to cover all possible failure points and still focus on what’s important inside the method, leaving what’s secondary on a separate logic track.
The cool thing is that emojis really help the eye identifying the different paths, so you can switch from one to another with very few cognitive effort.
Great! Your Service is now ready to go live :-)
Or not. You still need to add tests.
Big news: you should not unit test your Services. Simple as that.
Unit testing, as commonly understood, is counterproductive on Services.
“Whaaaaat?!” — I can already hear the indignant scream.
Let me explain that.
First of all, you definitely want to test your Services somehow. You owe it to your stakeholders. There’s no escape from that.
What you don’t want to do is to make your code convoluted, rigid and resistant to change.
Because this is exactly what happens when you unit test a Service.
By nature, a Service is mostly an orchestrator. It’s an object that executes steps in sequence by delegating to its dependencies.
The Service sequence has many branches too.
In order to unit test all possible branches, you would end up mocking everything, setting expectations all over the place and have a very rigid and bloated testing setup.
You would basically end up replicating a good part of your Service logic inside the test mocks only for testing one particular branch.
Another reason for not unit testing Services: accidental complexity.
Go full unit test on your Service flow means that you must abstract how an Entity gets created and enters its lifecycle.
If you can’t control that from the outside, it’s impossible to really unit test the Service.
So what you would end up doing is you would start introducing Entity Factories.
Entity Factories are an anti-pattern in the Domain Service context because the creation of an Entity is a full responsibility of the Service in its role of “Entity Manager”.
You should have all the data that you need in order to create a new Entity in your endpoint parameters and you should not delegate this responsibility to a dependeny.
The best testing strategy for your Services is use case driven.
Set optional preconditions, run the use case, evaluate the result.
This strategy can be implemented in different ways — one being the given, when, then structure — but please do not:
Your tests should be as simple as:
You should also turn-off the event publishing, since this would probably trigger logic outside the scope of the test.
In some sense, this is a unit test. The unit, in this case, is your Service as a whole.
Congrats! Now your Service can really go live :-)