A deep dive into the core of your codebase.
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.
What is a Domain Service?
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.
The anatomy of a Service
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:
- the arguments of the method call are translated into domain objects;
- cross-Entity business constraints are enforced;
- the relevant piece of business logic is executed on a specific Entity;
- resulting changes are published outside the Service boundaries;
- the outcome of the method call is communicated back to the Interface.
Let’s go through the steps one by one.
1. Argument translation
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:
2. Cross-Entity constraint protection
Let’s add a couple more rules to your fictional domain:
- Owners can be activated or deactivated;
- Is not possible to add new B&Bs for deactivated Owners.
#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:
3. Entity manipulation
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:
- the Service starts by loading the subject Entity from the related Repository, if the Entity already exists (not your current use case);
- if the Entity exists, the Service calls the specific Entity method related to the current use case, causing an internal change in the state of the Entity;
- conversely, if the Entity doesn’t exist already, the Service simply creates it using the class constructor or a semantically relevant static factory method;
- the Service saves the Entity back to the Repository where the updated state will be serialized.
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:
4. Publishing the changes
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.
5. Responding to the Interface
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?
Enters the Result
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).
What if anything goes wrong?
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:
- the Service creates a typed Result by the get factory method.
- if everything goes fine, the Result object will be of type Success and will contain the business subject (the ReadOnlyBnb or the Room); therefore, by implementation of the Success class, the ❌ method call won’t produce any effect;
- if any exception is thrown, it will be captured by the get factory method and a Failure object will be returned instead; the ❌ method will be executed and the failure logged. From now on, the Result will be of type Aborted, which means that both the ✅ and the ❌ methods won’t produce any effect;
- later on, assuming that the business logic was successfully executed, the ✅ method is used a second time to publish the related event;
- if something goes wrong in the event publishing step, the second ❌ method will be executed and the partial failure logged;
- finally, the Result is returned. It’s important to note that the call will be considered successful if at least the first part of the method is successful (the change of state in the subject Entity). Whether we succeed or fail in publishing an event is not relevant for the returning value.
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.
So what’s the best strategy for testing Services?
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:
- use cumbersome testing tools;
- test through any of the Interfaces;
Your tests should be as simple as:
- setting up the part of the infrastructure needed by the Service. This is typically a storage layer;
- instantiating the Service inside your test;
- send optional commands to your Service to bring it in the desired state and fulfill the preconditions;
- send the use-case command and get a result;
- evaluate the result.
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 :-)