Building an Application with TDD, DDD and Hexagonal Architecture — Isn’t it a bit too much?
A few weeks ago I was honored to be presenting this very session at Booster Conference, in the beautiful Norwegian city of Bergen.
I humbly welcomed a large crowd of developers to join and listen to my experiences in applying and reflecting on some of the well-known principles in software development. I admit that having a few of “buzzwords” in the title of the presentation would have triggered the audience’s curiosity nevertheless. However I do believe that we, as a community, are truly interested in improving our understanding of the needs behind the core principles and guidelines we regard as best practices. That is precisely what inspired me to create this presentation.
A small token of being convinced that this discussion is indeed of some value was the following question I got during the session:
How did you reason about the control flow of error handling? Was it outside the scope of the domain model with exceptions simply propagating? Did you consider adding error states to the domain model?
A great question touching upon the very interesting topic on error handling in the domain logic. I remember that my first attempts of isolating domain models from the rest of the application ignored this issue completely. The exceptions were simply propagated, as the talk attendant asking the question implied, and handled outside in some generic manner.
However, with experience there came a realization that an important piece was missing to be able to craft more precise models. In particular, when the error in domain logic was representation of some circumstance that domain experts could relate to.
Take a look at the code example from the presentation. How do we set up control flow of error handling, as the question stated?
Before going further, please note that we are not talking about any kind of errors or exceptions that might arise. We are focusing on error states where the business flow encounters an unexpected situation.
// omitting return types for brevity
public SynchronizationSummaryForLogging WithHREmployeeService()
{
buildEmployeesByOrganization(); // what happens if error occurs here?
reflectOrganizationStructureInSMSGroups();
synchronizeEmployeesAndOrganizationsWithSMSGroups();
// currently all error states are reported/logged in a summary
buildReport();
}
Our approach here is to catch any error state and include it in the summary report. That is perfectly valid approach, drawing some inspiration from functional programming. We allow normal flow to finish and then we summarize all the errors that might have occurred during the processing in a report.
However, what if there are business cases where an error state must interrupt normal flow?
@NotNull
private Organizations buildEmployeesByOrganization(EmployeeService employeeService) throws URISyntaxException, IOException, InterruptedException {
var employees = employeeService.fetchEmployees();
var organizations = new Organizations();
// What if something unexpected happens in this method?
organizations.BuildEmployeesByOrganizationsFrom(employees);
logCreatedGroups(organizations);
return organizations;
}
There are a couple of possible alternative flows that could be regarded as business error states occurring during organizations.BuildEmployeesByOrganizationsFrom(employees)
call:
- What happens if there is an organization without employees?
- What if an employee belongs to two different organizations?
- What if we have a freelance employee without any direct organizational affiliation?
For these situations to be classified as error states, the business needs would have been formulated stating that the further processing must stop if any of the above-mentioned should occur. Such business model could exist in cases where data consistency is of paramount importance.
How can we express this business model in the code?
Enter Domain Exceptions!
If an error state is something that domain experts care about then we model it explicitly in the code as a part of the domain model
This approach further enriches the domain model, bridging the expression gap between the code and the human conversation.
public class OrganizationWithoutEmployeesException extends Exception {
public OrganizationWithoutEmployeesException(String message) {
super(message);
}
}
// More domain exceptions defined the same way:
// EmployeeInMultipleOrganizationsException ....
// EmployeeWithoutOrganizationException ....
// Throwing domain exception as part of the domain logic
public class Organizations {
public void buildEmployeesByOrganizationsFrom(List<Employee> employees)
throws
OrganizationWithoutEmployeesException,
EmployeeInMultipleOrganizationsException,
EmployeeWithoutOrganizationException {
// domain logic ....
}
}
The code in the domain model defines and then simply throws a Domain Exception, thus clearly stating what business error had occurred, interrupting the business process. The responsibility of catching the exception and conveying the meaning of the error to human is left to the rest of the application to deal with.
Notifying a human user could mean, in case of a web application, converting exception types to REST status codes. In other cases, if possible to implement a model as a state machine, each Domain Exception could put the application into a dedicated error state to be detected through monitoring.
Conclusion
Reiterating the original questions from the conference
How did you reason about the control flow of error handling? Was it outside the scope of the domain model with exceptions simply propagating? Did you consider adding error states to the domain model?
The suggestion is to handle this with respect to business requirements. Either the requirements allow errors to be reported whilst continuing the business processes or specify that each error should stop the business process.
In the conference talk, the original implementation collected error states as a summary to be reported to human user after the business process finished. The approach was to create a dedicated class whose instance would contain all the encountered errors and then pass that instance to the error reporting functions. The same could have been achieved by publishing events for each error occurrence and continuing the processing. The report function would, in that case, have taken a role of event consumer.
In this article, as an alternative, we presented modeling each error occurrence as a Domain Exception to document the needs to interrupt the business process and notify human user immediately.