Implementing Clean Architecture - Case Study: Sending e-mails
02 Feb 2022During my day job we recently did a code review of a small feature of an application which aims to follow the principles of Clean Architecture. At a first glance separation of concerns as well as basic principles of Clean Architecture were followed. But a closer look revealed that some dependencies did not follow the Dependency Rule, specifically there were dependencies from adapters layer to the frameworks layer.
During our design discussions which followed this observation I realized that this example would perfectly serve for a case study on how to design a feature in Clean Architecture in detail.
So here we go …
Setting the context
The application, this feature is designed for, analyzes test failures from our CI/CD pipeline (stored in some test database) and creates defects in our defect database. The application also has a simple configuration file containing e.g. properties to fill certain defect fields like “Assigned To”.
The feature we were reviewing is supposed to send e-mails to our development teams in case some test failures could not be processed properly because of e.g.:
- Some network failure broke the whole CI/CD pipeline execution
- An internal error in our application happened (e.g. due to corruption of the configuration file or a programming error)
- The database storing the test failures from our CI/CD pipeline is down
Iteration 1 - Getting started
In line with agile principles let’s develop the design incrementally starting with the “core” of any application: the business logic.
As all application specific business logic is organized in Use case interactors
in Clean Architecture we create a new one called MailNotificationInteractor
which has the
responsibility to detect the scenarios mentioned above. The MailNotificationInteractor
could be triggered
through an API call from the actual test failure processing interactor or it could receive a notification through
some messaging mechanism inside the application. For the scope of this post we will not elaborate this aspect any further.
Instead, let’s explore the responsibility of the MailNotificationInteractor
. It
- Detects what exactly happened
- Decides which concrete information should be notified to whom
- Fetches all relevant information e.g. from test database, application log and configuration file
- Composes a respective e-mail
- Triggers sending the e-mail by an e-mail server
Of course our interactor would not talk to any of these “external devices” (test database, configuration file,
e-mail server) directly as this would create a dependency from the use case layer to some outer layer which would
violate the Dependency Rule. Instead, we create interfaces for all these collaborations in the use case layer and design
those to be most convenient for the MailNotificationInteractor
, e.g. we create specific APIs on the configuration
repository to read all relevant information for creating and sending e-mails.
Furthermore, we decide that the MailNotificationInteractor
will create different e-mail response objects
representing the different scenarios detected in the application and containing specific detailed information about those.
For instance, in case of a network issue the IT experts should be informed about the concrete impacted test agents and
in case of some internal application error the exception details have to be part of the e-mail.
This is the design we end up with after the first iteration:
Iteration 2 - Communicating to the outer world
In Clean Architecture the test database, the configuration file as well as the e-mail server are considered as
“external devices” which are located in the most outer circle, the Frameworks layer.
In order to allow collaboration of the MailNotificationInteractor
with these external devices
we use the Dependency inversion principle.
As we created the required interfaces already in the previous iteration we only need to provide
one implementation each inside the frameworks layer which then talks to the respective external device.
The MailClient
for example would take the different e-mail response objects, builds up an
System.Net.Mail.MailMessage
and passes it to System.Net.Mail.SmtpClient
which passes it to
the actual e-mail server to deliver the e-mail.
The remaining piece of the puzzle is a component which wires up all implementations with the interfaces. This component Uncle Bob calls the “Main component”:
The Main component is the ultimate detail - the lowest-level policy. It is the initial entry point of the system. Nothing, other than the operating system, depends on it. Its job is to create all the Factories, Strategies, and other global facilities, and then hand control over to the high-level abstract portion of the system.
— Clean Architecture, Robert C. Martin (aka: “Uncle Bob”)
The design now looks like this:
Iteration 3 - Formatting HTML e-mails
Ok, so far so good. The application can now send e-mails and all dependencies follow the Dependency Rule of the Clean Architecture.
But then it happens - as usual - just as we have finished our nice design the requirements change: instead of pragmatic but not so pretty ASCII e-mails, the customers demand nicely formatted HTML e-mails …
So which component in the current design is responsible for creating the e-mail body and should do all the HTML formatting? It is clearly not the responsibility of the interactor - “formatting” is clearly not part of the business logic, especially not any formatting for a specific presentation technology like HTML.
What about the MailClient
? Dependency-wise this could be an option but remember that we want as less
code in frameworks layer as possible in Clean Architecture because we want as less code as possible depending
on external devices and external frameworks and we would have a hard time testing code with such dependencies.
Actually, the best fit for formatting something would be a “presenter” component. Such a component
would be located in the adapters layer in
Clean Architecture. So we create a MailClientAdapter
which creates a view model called HtmlMail
which - in contrast to the response objects of the interactor - is pretty generic and is designed to be very
convenient for the MailClient
to be processed (e.g. it as a “Body” property containing a completely
formatted HTML content as string).
The MailClientAdapter
now implements IMailClient
interface defined in the use case layer and
defines a new IMailClient
interface which consumes the HtmlMail
view model and which is
implemented by the MailClient
in the frameworks layer.
(I know it is a poor design choice to use the same name for two interfaces in two different layers
but I couldn’t come up with a better name so far. Do you have one? Leave a comment below …)
Finally we will change the Main component accordingly.
Iteration 4 - Adding some tests we should …
Of course we could have - and maybe even should have? - started the whole feature by writing the tests first. On the other hand, this post is not about test driven development but about how to design a feature according to the Clean Architecture. So we will also not dive into test design or test engineering in this section but rather explore how we would add testability to this feature.
One of the key questions is: How will tests interact with the application in general and the e-mail feature specifically? Should the tests interact with the UI? Should the tests interact with the business logic APIs, the interactors? Not being convinced at first, today I fully agree with Uncle Bob on the need for a “Test API”:
The purpose of the testing API is to decouple the tests from the application. This decoupling encompasses more than just detaching the tests from the UI. The goal is to decouple the structure of the tests from the structure of the application.
[…]
This API will be a superset of the suite of interactors and interface adapters that are used by the user interface.
— Clean Architecture, Robert C. Martin
Furthermore, the Test API will provide fake implementations for all components in the frameworks layer so that the tests do not depend on external devices whose behavior is unpredictable.
This setup enables simple and fast tests, focusing on testing as well as documenting the scenarios of the e-mail feature from customer perspective (see BDD).
Conclusion
Actually pretty straight forward, isn’t it? ;-)
The key - from my perspective - is to just follow:
- Separation of Concerns (decision making, formatting, external devices)
- Dependency Rule
Update
In this video I show how an implementation of this design could look like:
The "Implementing Clean Architecture" series
- How to implement the Clean Architecture?
- Implementing Clean Architecture - An Overview
- Implementing Clean Architecture - Make it scream
- Implementing Clean Architecture - What is a use case?
- Implementing Clean Architecture - Of controllers and presenters
- Implementing Clean Architecture - Are Asp.Net controllers "Clean"?
- Implementing Clean Architecture - Frameworks vs. Libraries
- Implementing Clean Architecture - Case Study: Sending e-mails
- Implementing Clean Architecture - To use or not to use MediatR?