Introduction
My first contact with clean architecture was in my 5th semester when I have studied computer science. There I learned the basics of all principles and the other details, that are relevant for clean architecture. Until then, one question was unanswered: How can annotations (e.g. JPA annotations) be avoided in the domain layer. Between the research about this topic, I stumbled across a article, which at this point I can highly recommend: https://www.baeldung.com/spring-boot-clean-architecture. Reading this article has been so compelling for me, that I had to write my own and share my own experiences with you.
Before we have a deeper look at clean architecture, I want to point out one important information for this article. There are chapters prefixed with NTK, which is the short term for "Nice to Know". This chapters are only for the audience interested in small details, if you are new to clean architecture, feel free to skip those.
Clean Architecture
I already assume, that you are familiar with the basic concept of clean architecture. Due to this fact, I will only show an overview diagram, which will be the base of how we will structure our code (Leaning on Uncle Bob's Clean Architecture):
As you can see in the figure above Clean Architecture divides source code into four layers:
- Plugin layer
- Adapter layer
- Application layer (also known as use case layer)
- Domain layer (also known as entity layer)
Each of these layers have different responsibilities and all of our classes will be placed inside it's certain layer. I will not go into detail here, because after we will talk about the use case in the next chapter, the layers will be described individually.
Important are the white arrows, that crosses the boundaries between the different layers. This represents the main rule of clean architecture - the dependency rule. Dependencies of a source code can only reference from the outside to the inside. That means that, for example, classes inside the use case layer can reference classes in the entity layer, but entities cannot reference use cases.
On the right side of the image is a diagram titled "Code relations". This diagram describes the relationships between the classes within the different layers. In the center there is the Interactor, which has references to the Use Case Input Ports and Use Case Output Ports. The Interactor is located in the application layer, which means that the two references Use Case Output Ports and Use Case Input Ports are mapped by interfaces. The controller, which is located in the adapter layer, has a reference to the Use Case Input Port, which is located in the application layer. Within the application there will be a number of Use Case Output Ports, the Presenter shown is just an example of one. This Presenter implements the interface, which is in the application layer, in order not to violate the main rule of clean architecture.
The flow of control through the application is indicated by the petrol-coloured arrow. The controller is triggered by an event that is controlled by a user/system. Once this happens, the interactor is used to execute the use case. During execution, the interactor makes use of the output ports, that are connected to it.
We use this structure as a template for our entire application. Furthermore, we use the terms Presenter, Controller and Interactor as naming conventions for the class names of our implementation. We will change the terms Use Case Input Ports and Use Case Output Ports to InputBoundary and OutputBoundary, because I think these are better names to describe the role of the components.
This was a lot of theory, let's head to our use case, which will follow us across the whole article
Use Case
In our fictive scenario marketing wants an application to create products. Discussions show that there are two types of products. On the one hand the base products and on the other hand the technical products. The base products consist of the following attributes:
- id
- name, which must never be empty and must be greater than five characters.
- description of the product
- price
- creation date (Presentation as follows: yyyy-mm-dd)
In addition to the attributes of the basic products, the technical products consist of the following additional attributes:
- technical information, which must never be empty
- instruction manual
Marketing also defined user stories to define the functionality of the application:
- The application receives all information about a base product, validates if the product already exists, and saves the new product
- The application receives all information about a technical product, validates if the product already exists, and saves the new technical product
At this point you have probably noticed that we have not mentioned a databases, UIs or other technologies. This is either because marketing hasn't made a decision yet or you and your team haven't decided on any technology. Either way, our business logic doesn't care about these details, neither should our code.
Now that our use case is defined, we can almost start coding, but first we need to talk about how we structure source code.
Code Structure
Depending on how our application will be delivered, we have to decide how our code will be structured. There are two options here:
- Monolithic - divided by packages
- Usage of modules - e.g. make use of maven (recommended)
With each of this options we can achieve the goals of clean architecture. Depending on business requirements the strategy might change. Despite the fact, that I would suggest to use modules, because there you can setup a strict separation of each layer, we will use a monolithic approach for this article.
Description of layers
The following chapters will contain detailed information about all layers, from the inner to the outer layer. Each chapter contains references to programming principles, all of which are relevant to Clean Architecture.
Domain layer
The following illustration, which represents the domain layer as a class diagram, provides an initial overview:
The domain layer contains the business rules of the application. Due to the fact that the content of this objects will be saved in a database, I will call them entities from now on. The domain layer is a great place to apply different programming patterns. To name one of them, the domain-driven language should be considered. Let's take a close look at the IProduct entity (upper left corner in the figure).
public interface IProduct {String getId();String getName();String getDescription();double getPrice();boolean nameIsValid();}
This interface contains get methods for all attributes, that are relevant for the use case. It also contains nameIsValid to represent the first business rule, which says: "A name of a product should not be empty and at least have 5 characters". The second business rule is represented by the ITechnicalProduct interface. To avoid that Clients reference interfaces that they do not use, we separate those functionalities to two separate interfaces (more about Interface Segregation Principle).
public interface ITechnicalProduct extends IProduct{String getTechnicalInformation();String getInstructionManual();boolean technicalInformationIsValid();}
First of all note that the interface inherit from IProduct, because it needs all dependencies from it's predecessor, but the interface provides two new attributes, which correlates with the second business rule: "Technical information should never be empty". With this two interfaces we also adhere the Liskovs Substitution Principle (LSP).
Let's have a look at the implementation of both interfaces:
public class CommonProduct implements IProduct{private String id;private String name;private String description;private double price;private static final int MINIMUM_CHARACTER_LIMIT = 5;public CommonProduct(String id, String name, String description,double price) {this.id = id;this.name = name;this.description = description;this.price = price;}@Overridepublic boolean nameIsValid() {return name != null && name.length() >= MINIMUM_CHARACTER_LIMIT;}// getter and setter}
public class TechnicalProduct implements ITechnicalProduct{private String id;private String name;private String description;private double price;private String technicalInformation;private String instructionManual;public TechnicalProduct(String id, String name, String description,double price, String technicalInformation,String instructionManual) {this.id = id;this.name = name;this.description = description;this.price = price;this.technicalInformation = technicalInformation;this.instructionManual = instructionManual;}@Overridepublic boolean technicalInformationIsValid() {return !this.technicalInformation.isEmpty();}// getter and setter}
Those two implementations are straight forward programming, so there is not much to say here.
In the domain layer, we also have factories, because of two reasons. The first reason is to stock the stable abstraction principle (SAP) and the second is to isolate the product creation. Following lines of code show the interface and implementation of the CommonProduct factory:
public interface ICommonProductFactory {IProduct create(String id, String name, String description, double price);}
public class CommonProductFactory implements ICommonProductFactory {@Overridepublic CommonProduct create(String id, String name, String description, double price) {return new CommonProduct(id, name, description, price, Instant.now().toEpochMilli());}}
You may see no value in isolating the product creation, until you have to test complex creations of complex object, which you want to test before you continue with your business logic. Testing is the keyword for the next chapter, where we will test our entities.
NTK: Interface vs Abstract Class
At this point at the latest you will surely wonder and ask yourself "why didn't he create one abstract class for both of the products an one additional interface for the technical product". To save lines of code this is certainly true, because if we decide to implement it with an abstract class the repetitive code would be inside the abstract class. However there is a drawback to this approach. Assuming that we want to use technical product in one of our future classes, we would have an interface similar to this:
public interface ITechnicalProductAdditionToAbstractClass{String getTechnicalInformation();String getInstructionManual();boolean technicalInformationIsValid();}
If we want to use the interface we would write the following lines of code:
public boolean doSomething() {ITechnicalProductAdditionToAbstractClass technicalProduct = new TechnicalProduct("ValidID", "Name", "Descr", 50.0d, "Some technical details", "Instructions");String instructionManual = technicalProduct.getInstructionManual();String name = technicalProduct.getName() // !!! this won't work !!!}
The doSomething method highlights the problem with this approach. With ITechnicalProductAdditionToAbstractClass only containing the methods of a technical product we can not access the methods of IProduct, because it is simple not related to each other. Another argument may be to use the implementation of ITechnicalProductAdditionToAbstractClass, because there we have the relation it extends the abstract class and also would implement ITechnicalProductAdditionToAbstractClass. This is a valid point, but our goal is to code against an interface, because with this we can change the behavior of a program at runtime (which is not relevant in our simple scenario) and it also helps to maintain the code. It also may be, that technical products have an prefix or suffix inside the name for whatever reason, with this requirement we would have to split at least the property 'name'. In a perfect world the last argument should never happen, because a domain should be defined once, but in reality this happen quite frequently.
Long story short, in my opinion we write code not to optimize the lines of code, but to extend and maintain it as easy as possible. This is also clear from the principles and explanations of Clean Architecture, so the following rule is the conclusion of the discussion:
Single Responsibility Principle (SRP) > Don't Repeat Yourself (DRY)
Unit Testing our domain
Now we are heading a topic where I have had the experience that many developers and customers don't like: testing Code. One of the main reasons to use clean architecture is to increase maintainability, to prove this, we are going to the domain and application layer. Let's take a look at the CommonProductTest:
@Testpublic void test_givenAbcName_whenNameIsNotValid_thenIsFalse(){IProduct product = new CommonProduct("Abc", "Name", "description", 0.5d);assertFalse(product.nameIsValid());}
Tests in the domain layer are quite simple as you can see from the example above. In addition the absence of Mocks is a good sign for this layer. If you are thinking about mocks here, then you are probably mixing some layers.
Application Layer
The application layer contains a large number of components; the following illustration shows these components and their relations:
The first part are the boundaries, which itself consists of two further parts: inputs and outputs. The second part are the interactors, which can be described as Conductor of applications. The third part are the models that are responsible for representing data from requests and responses. All of these components focuses on one single goal: automatize our business logic. Let's dive into the interactors.
Interactors
Interactors are the main part of every application's business logic (seen in the middle of the application layer in the figure above). They describe the automatization of business rules. They hold a lot of dependencies in order to achieve this automatization. Before we look at the whole source code of the Interactor, let's focus on the instance variables and the constructor:
public class CommonProductInteractor implements ICommonProductInputBoundary {ICommonProductPresenter presenter;ICommonProductFactory factory;ICommonProductRegisterGateway gateway;public CommonProductInteractor(ICommonProductPresenter presenter, ICommonProductFactory factory, ICommonProductRegisterGateway gateway) {this.presenter = presenter;this.factory = factory;this.gateway = gateway;}
The first thing you will notice are the instance variables, which represent the dependencies, that are present on the top of the class. They enable the dependency inversion principle (DIP). Due to the fact that we only reference interfaces, which are later implemented by different classes in the outer layers, we also fulfilled the requirement necessary for DIP "Abstractions should not depend upon details. Details should depend upon abstractions".
Now that we have our boundaries we need to use them. For this purpose we implement the ICommonProductInputBoundary, which you hopefully remember, exposes unique functionality from our use cases to the outer layer. This enforces us to implement the following method:
public class CommonProductInteractor implements ICommonProductInputBoundary {// instance variables// constructor with dependency injections@Overridepublic CommonProductResponseModel create(CommonProductRequestModel requestModel) throws ProductCustomException {if (gateway.existsById(requestModel.getId())) {return presenter.prepareFailView(new ProductAlreadyExistsException("Product with id " + requestModel.getId() + " already in database"));}IProduct commonProduct = factory.create(requestModel.getId(), requestModel.getName(), requestModel.getDescription(), requestModel.getPrice());if (!commonProduct.nameIsValid()) {return presenter.prepareFailView(new InvalidNameException("Name " + commonProduct.getName() + " is not valid"));}gateway.save(commonProduct);CommonProductResponseModel responseModel = new CommonProductResponseModel(commonProduct.getId(), commonProduct.getName(), commonProduct.getDescription(), commonProduct.getPrice(), String.valueOf(commonProduct.getCreatedAt()));return presenter.prepareSuccessView(responseModel);}}
First of all you see the argument requestModel of the create method, it is of the type CommonProductRequestModel, which holds all data, that is coming from the product request. This is the base of our method, now we can manipulate and transform this data to fit all of our use cases.
The first system rule was that the system validates if a product already exists. The first 3 lines of our code covers exactly this rule. Beginning with the if statement which uses the database gateway to check, if a product, with the id of the given requestModel, is already in the database. If this is true a other output boundary will be used - the ICommonProductPresenter.
The presenter is responsible for two things. The first thing is to provide information about an error that may occur during a step in our interactor. The second thing is the exact opposite; provide information about the success of a set of operations. To give more insights, we look at the throw declaration of the method. In case of create method it throws a ProductCustomException. I intentional gave the name ProductCustomException to emphasis that it is a custom exception written by us. In my opinion a method that we create should throw exceptions that we made. So if a product already exists a ProductAlreadyExistsException with detailed information will be thrown.
If we however pass the condition, our factory will create an IProduct with the information provided by the request. Now that we created a IProduct, we can check our business rule: "A Product name must have more than five characters". Again if this condition is true the presenter will be used to provide detailed information about the error. But if this condition is also fulfilled, we will use our gateway to finally save our product to the database.
However this was not the end of our method, because we have to give a response to our requestor. We will do this by creating a *CommonProductResponseModel" which represents the data, that we want to give as a response.
My experience is that in productive software there is always a distinction between data that is saved in a database and data that will be presented. An example is the 'createdAt' attribute, which can be stored in many different ways, because time can be represented in various ways. Due to that, in my opinion it is always better to save it as unix timestamp, because it is independent from any time zones. But for a representation on a UI, an unix timestamp is not best fit. For this product creation we set the presentation rule as follows: Product response must contain the date of creation in following format: yyyy-mm-dd. This should be the responsibility of the presenter, which is the reason why CommonProductResponseModel is an argument of the prepareSuccessView. The implementation of the presenter will format our 'createdAt' date (see later chapters).
Now we are at the end of our interactor. If you read the interactor and thought from early on: "Hm, this looks a lot like our use case" then you can be proud of yourself, because that's should be the outcome. You can read the interactor as you would read an requirement.
We will not go over the TechnicalProductInteractor, because it is the same as the CommonProductInteractor with other gateways and one more business rule (see git repository for more information).
In- and output boundaries
In general boundaries are contracts defining how components can interact. Input boundaries exposes our use cases to the outer world. Output boundaries should be used to make use of the outer layers. Basically the in- and outputs only consists of interfaces. To give an example, we will look at the IProductExistsGateway and ICommonProductRegisterGateway:
public interface IProductExistsGateway {boolean existsById(String id);}public interface ICommonProductRegisterGateway extends IProductExistsGateway{void save(IProduct iProduct);}
In this layer we don't care about details and therefore there are only interface definitions. The important part here is that we describe the data that the outer layers will use or provide to fulfill the functionality.
In chapter Interactors we already looked at the implementation of one input boundary we defined. So I will skip this part here and continue with request and response models.
Request and Response Models
In the previous chapter we used classes that were called request and response models, but haven't looked at them closely yet. They exist to transfer data across borders. The request models of both technical and base Product are quite similar to the entities, due to the fact that we have simple example application. In my experience it is not recommended to mix entities with request models and use them for the same purpose, because they have different responsibilities. Therefore they will change for different kind of reasons.
Although it should be split into several classes, we will only take a closer look at the CommonProductResponseModel due to the simplicity:
public class CommonProductResponseModel {private String id;private String name;private String description;private double price;private String createdAt;public CommonProductResponseModel(String id, String name, String description, double price, String createdAt) {this.id = id;this.name = name;this.description = description;this.price = price;this.createdAt = createdAt;}// getter and setter}
As you can see, there is not much to mention here. The only thing that is different from the entity class is that the createdAt property in the ResponseModel is of type String, as we will format the response, because of the system rule.
With this said, we can test our application layer.
Test our application layer
It is no secret that we have to mock several things on our unit tests in the application layer. At this point I was asking myself: "what do I have to mock and what not?". To strictly fulfill the Unit Test characteristics we have to mock each dependency, but why?
There are several reasons for this. The first one is because of the ongoing development of applications. The project could split up to several work packages and one of them could be the implementation Gateways. But if a other team is currently working on the application layer, then this team has to wait, until the development is finished. With Mocks you can just mock the API of this Gateways.
Another reason to use mocks is, because they are deterministic and with that they always behave exactly the same, which is very important when testing a system. The last advantage I want to point out is, that it enforces us to program against interfaces and therefore help us to adhere the rules of clean architecture.
With all this said let's have a look at the first half of our CommonProductInteractorTest:
public class CommonProductInteractorTest {ICommonProductFactory mockedFactory;ICommonProductPresenter mockedPresenter;ICommonProductRegisterGateway mockedGateway;@BeforeEachvoid setup(){mockedGateway = Mockito.mock(ICommonProductRegisterGateway.class);mockedFactory = Mockito.mock(ICommonProductFactory.class);mockedPresenter = Mockito.mock(ICommonProductPresenter.class);}
As discussed the first thing we need to do, is to mock our interfaces, that the interactor will need to execute the business logic. In this example we will use Mockito as our framework. Therefore we need to tell Mockito to mock the actual interface, in the above code snippet, you see this in the setup method. We need to do this before each method we want to test, that's why the setup method has an @BeforeEach annotation.
With this things set up, let's have a look at a real test:
@Testvoid givenValidCommonProductProperties_whenCreate_thenSaveItAndPrepareSuccessView() throws ProductCustomException {// ARRANGECommonProductRequestModel requestModel = new CommonProductRequestModel("TestId", "ValidTestName", "Test description", 52.2);long timestmap = 1668617824L;CommonProduct product = new CommonProduct("001", "ValidName", "Some Description", 25.25d, timestmap);CommonProductResponseModel responseModel = new CommonProductResponseModel(product.getId(), product.getName(), product.getDescription(), product.getPrice(), String.valueOf(product.getCreatedAt()));CommonProductResponseModel finalResponseModel = new CommonProductResponseModel(product.getId(), product.getName(), product.getDescription(), product.getPrice(), "2022-11-16");Mockito.when(mockedGateway.existsById(requestModel.getId())).thenReturn(false);Mockito.when(mockedFactory.create(requestModel.getId(), requestModel.getName(), requestModel.getDescription(), requestModel.getPrice())).thenReturn(product);Mockito.when(mockedPresenter.prepareSuccessView(responseModel)).thenReturn(finalResponseModel);CommonProductInteractor interactor = new CommonProductInteractor(mockedPresenter, mockedFactory, mockedGateway);// ACTCommonProductResponseModel verifyResponseModel = interactor.create(requestModel);// ASSERTAssertions.assertEquals("2022-11-16", finalResponseModel.getCreatedAt());Mockito.verify(mockedGateway, Mockito.times(1)).save(product);Mockito.verify(mockedGateway, Mockito.times(1)).existsById(requestModel.getId());Mockito.verify(mockedPresenter, Mockito.times(1)).prepareSuccessView(responseModel);Assertions.assertEquals(finalResponseModel.getId(), verifyResponseModel.getId());Assertions.assertEquals(finalResponseModel.getName(), verifyResponseModel.getName());Assertions.assertEquals(finalResponseModel.getDescription(), verifyResponseModel.getDescription());Assertions.assertEquals(finalResponseModel.getPrice(), verifyResponseModel.getPrice());Assertions.assertEquals("2022-11-16", verifyResponseModel.getCreatedAt());}
We are looking at a test, that goes through a creation of a product successfully. I strongly recommend naming tests sensible, like you see in the example above (if you want to read more about meaningful names). For unit testing I always use the AAA-Pattern.
With this given, we look at the first A - Arrange. This code will setup our data models as well as the entity, which will later be used by the factory. One important arrangement is the initialization of the CommonProductInteractor with all the mocked interfaces from the init method above. Now we can call the method we want to test, which is the next A - Act. With interactor.create(requestModel) we will execute our business logic. Now only one A is missing - the Assertions. Here we will check, if the actual output of our method call is the output we wanted it to be. It is very straight forward, so I will not go into detail here. However there are one thing that is really interesting, if you want to read about it, read the nice to know chapter: Accessing instances created during execution.
Of course the TechnicalProductInteractor has to be tested too, but because they are so similar to each other, I will leave this part out. If you are interested, once again I refer to the github repository.
As well as the interactor code, the tests are also straight forward and easy to implement. We only looked at the successful creation of a product, of course there are more cases to test. If you are interested in other cases, also check out the repository.
At this point I want to mention one thing: Until now, we didn't start the application once. Despite this fact and with our tests, we have the confidence that it will work as we intended, because we tested it already. In my opinion permanent testing, with starting up the application and manually triggering the requests is the completely wrong approach.
NTK: Assert instances created during execution
There are several things you will encounter, when unit testing your application. There are many use cases where you will have to create a new instance of an object inside of a method you want to test. Inside the CommonProductInteractor there is exactly this scenario, because we create a CommonProductResponseModel.
Now we have two options to solve the problem, that we can't access this instance. The first solution is to overwrite the equals method of the specific class it instantiated. There we need to change or remove the comparison of instances. When we removed it, we can assert the instance, with the instance we created in our test case, because now it only compares the properties of both instances and not if they are the same instances. In the previous chapter, I used this approach to make it as easy as possible to read.
However this cannot be done in any use case. Therefore we have a second solution. Mockito has a class called ArgumentCaptor. With this class it is possible to retrieve the created instance of a class during execution of a test. With the created instance during the test, we can assert both properties manually with the instance we created for our test and the instance of the ArgumentCaptor. If you want to read more about this topic, I recommend following article.
Both of the solutions solves the problem of accessing instances created during runtime.
Adapter Layer
At this point we have our domain objects and our use cases build up. One thing is still missing in our application - the usage of the outer layers. This is done by the adapters, following illustration shows how they are connected with our domain and application layer:
Connect persistence
Here we have to decide, what database we want to use for our project. In real scenarios this should be discussed with the whole team and should always use the requirements as basis of the decision. For this example we will use H2 as our database due to it's simplicity. This is visualized by the right half of the adapter layer in the figure above.
For this, we implement our interfaces to completely fullfil the dependency inversion principle. We have three output boundaries in our example code. One of them is the ICommonProductRegisterGateway, let's have a look at the implementation in the CommonProductCreationH2Gateway class:
public class CommonProductCreationH2Gateway implements ICommonProductRegisterGateway {@AutowiredICommonProductRepository _repository;public CommonProductCreationH2Gateway() {}@Overridepublic void save(IProduct iProduct) {CommonProductJpaMapper commonProductJpaMapper = new CommonProductJpaMapper(iProduct.getId(), iProduct.getName(), iProduct.getDescription(), iProduct.getPrice(), iProduct.getCreatedAt());this._repository.save(commonProductJpaMapper);}@Overridepublic boolean existsById(String id) {return _repository.existsById(id);}}
The first thing I want to point out is the name of the class CommonProductCreationH2Gateway. I intentionally used H2 in the name, because now it is recognizable at a glance, that we use H2 in our application. The second reason is the interchangeability of gateways. If someone decides to use another database, the only thing we need to do, is to create a new gateway and implement the corresponding boundary.
Now let's talk about the source code. Due to the implementation of the ICommonProductRegisterGateway we have to implement the save and existsById method. Let's have a detailed look at the save method. First of all we instance a class called CommonProductJpaMapper. This is necessary because we need JPA annotations for our H2 database to work. To enable this, we again have a data transformation. With this transformation, we keep the annotations out of our domain layer, which by the way solves my question in the fifth semester, which I mentioned in the introduction.
This is how the CommonProductJpaMapper looks like:
@Entity@Table(name = "CommonProduct")@Data@AllArgsConstructorpublic class CommonProductJpaMapper {@Idprivate String id;private String name;private String description;private double price;private long createdAt;}
Now back to the save method. To enable database connection we will use a ICommonProductRepository, which I will show you in the next section. The only thing we need to do is to call the save method from the repository and give the CommonProductJpaMapper as argument. That's it. The data will be persisted now.
Similar to the save method is the existsById method, with the difference, that we only pass the string argument directly to the repository, because simple data formats can cross boundaries without any data transformation.
We saw a lot of referring to the repository, the following code snippet describes the JPA repository:
@Repositorypublic interface ICommonProductRepository extends JpaRepository<CommonProductJpaMapper, String> {}
The fact that we use spring respectively JPA, the repository is quite simple. The only thing we need to do is to give the interface the @Repository annotation and pass the CommonProductJpaMapper as well as the data type of the id, which is string in this application.
Once again we will leave the ITechnicalProductRegisterGateway out and go straight to the next chapter.
Connect presenter
To summarize our actions until now, we connected a database to our system, however there are boundaries still missing to complete our application. With the ICommonProductPresenter we can format our response, to fit the requirements for our frontend, as well as throw meaningful exceptions. Inside the illustration in the previous chapter, you can find the implementation of the CommonProductPresenter on the upper left corner below the configuration classes.
The implementation of the CommonProductPresenter looks like follows:
public class CommonProductPresenter implements ICommonProductPresenter {@Overridepublic CommonProductResponseModel prepareFailView(ProductCustomException e) throws ProductCustomException {throw e;}@Overridepublic CommonProductResponseModel prepareSuccessView(CommonProductResponseModel responseModel) {LocalDateTime responseTime = LocalDateTime.parse(responseModel.getCreatedAt());responseModel.setCreatedAt(responseTime.format(DateTimeFormatter.ISO_LOCAL_DATE));return responseModel;}}
The method prepareFailView just redirects the exception, we created at the interactor, to the requestor. With this information, the requestor will know what happened in the system. Due to the fact that a requirement is to format the created date of a product with the ISO standard (e.g. 2011-12-03), we have to implement it. This will be executed by the prepareSuccessView, because in my opinion this should be the responsibility of a Presenter.
That was one implementation of a Presenter of a CommonProduct. Let's connect the next boundary - the controller.
Connect controller
Inside the figure in the previous chapter, the controller is located in the middle of the adapter layer. Inside the controller we make use of the spring framework, as well as of our defined input boundary:
@RestController("/commonProducts")public class CommonProductRegisterController {ICommonProductInputBoundary inputBoundary;public CommonProductRegisterController(ICommonProductInputBoundary inputBoundary) {this.inputBoundary = inputBoundary;}@PostMapping("/create")public CommonProductResponseModel create(@RequestBody CommonProductRequestModel requestModel) throws ProductCustomException {return this.inputBoundary.create(requestModel);}}
In our example we have two Controllers for the two creation use cases. One for CommonProducts, which you see in the snippet above and one for the TechnicalProducts. As always I will explain it with the CommonProductController and leave the TechnicalProductController out.
The Controller class is really straight forward. With the help of the @RestController annotation and the spring framework, we define a simple Controller with all of it's functionalities. Now we need to couple our use case to the controller, we do this by instantiating the ICommonProductInputBoundary. The only thing we now need to do, is to dependency inject the InputBoundary with the constructor of the controller class and then define a new POST route. Inside this post route we use the CommonProductRequestModel as data exchange from the requestor. With the @RequestBody annotation we don't need to do anything here, it will be automatically created with the body of the request, due to the spring framework functionality. Now we just have to call the create method from the inputBoundary and return the response, that's it.
Now we have successfully made use of the outer layers and almost finished our adapter layer. There is still one more thing we have to talk about.
Connect our system
Now we are heading a ugly topic - connecting our system. We have implemented all classes, so how do we connect them? Somehow the system must now, that for example the CommonProductInteractor makes use of the CommonProductCreationH2Gateway, but how do we achieve this? Dependency Injection is the keyword here.
We will make use of spring and create a class named BeanConfig, where we will connect all of our components to each other. To give some example, how it is done, the next code snippet shows the dependency injections for our CommonProduct use case:
@Configurationpublic class BeanConfig {public ICommonProductPresenter commonProductPresenter() {return new CommonProductPresenter();}public ICommonProductFactory commonProductFactory() {return new CommonProductFactory();}public ICommonProductRegisterGateway commonProductRegisterGateway() {return new CommonProductCreationH2Gateway();}public ICommonProductInputBoundary commonProductInputBoundary(ICommonProductPresenter commonProductPresenter, ICommonProductFactory commonProductFactory, ICommonProductRegisterGateway commonProductRegisterGateway) {return new CommonProductInteractor(commonProductPresenter, commonProductFactory, commonProductRegisterGateway);}}
The only spring annotation we have to use is @Configuration. With this, spring knows it should use this file to look for the injections. As you can see this configuration file is also really straight forward. We just tell spring which interface to instantiate and it's correlating implementation it should use.
If you are interested in the configuration for the other use case, check out the github repository.
Now we talked about the domain, application and adapter layer, but what about the plugin layer?
Plugin Layer
This will be the shortest chapter of all, because we don't code here usually. The plugin layer is described as the lowest level of connection to external agents. For example the H2 driver to connect to the database or the web framework. In our example we will use spring, so in order to start the application we need an entry point:
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})public class App{public static void main( String[] args ){SpringApplication.run(App.class, args);}}
The above code snippet shows a basic class to start the spring application.
Here it could be discussed whether the start class is not also within the adapter layers, because it also only makes itself spring. Either ways it is fine, because now we can start our application and be proud of ourselves.
NTK: Auto configurations in spring
In order to show the maintainability with clean architecture the project consists of CommonProductCreationMongoDBGateway to connect MongoDB with the application. What I didn't know was, that spring has the auto configuration feature, that automatically tries to connect with MongoDB as it detects it inside the pom.xml. We don't want to use the feature in our application, that's why the exclude annotation exists in the previous chapter.
Talking about trade-offs
The main rule of clean architecture is to only have references from the outside to the inside. However, sometimes it make sense to break this rule - I call this trade-off. I will explain a few trade-offs, that in my eyes, could make sense to break the main rule of clean architecture.
The first possible trade-off is to remove the bean configuration classes and use the @autowired functionalities of spring instead, so that spring will automatically handle our dependency injections.
The second trade-off we could do, is to use the @Transactional annotation of spring to enable transaction management. Otherwise we have to implement it ourselves (if this were a real productive software).
It is also possible to remove the boilerplate code from our domain or model classes with the help of Lombok. Lombok can be used to remove for example getter and setter methods and use annotations instead (more about lombok). This would enable us some "quality of life" features for programming our software.
All of this trade-offs would harm the main rule of Clean Architecture.
Despite this fact, the three trade-offs above are just a few examples of what trade-offs, that we could use in our application. All trade-offs come with the advantage, that we don't have to handle certain functionalities, but they come also with a downside. It will make it harder to maintain or rewrite some functionalities and therefore harm our maintainability. It is up to us respectively you and your team, to decide how much flexibility or work you want to maintain yourself or to leave it in the hands of others.
Conclusion
In this article we have learned about a clean architecture, defined a simple use case and built an application around this specific scenario, considering clean architecture in all steps. All of this principles where a lot to consider. The main question now is: what are the benefits of using clean architecture?
We build our application, that we can interchange all of the technology that are not decided yet or should at this point be not decided, such as databases or communication frameworks. Even tough we decide for a technology we can change it with minimal effect on the business logic of our application. To show this benefit, I implemented the CommonProductCreationMongoDBGateway to connect the application to MongoDB. The only thing we need to change is to tell the application to take the CommonProductCreationMongoDBGateway instead of CommonProductCreationH2Gateway and everything will work as intended. Feel free to check this by looking at the code inside the repository.
Furthermore, Clean Architecture increases the testability and thus the sustainability of our application, which we have seen in the test chapters.
Let us return to the question from the beginning: where to put certain requirements? As so often the answer is: "it depends". I give a few examples to get a feeling about what I mean by "it depends". For example marketing describes a new functionality that they want in the application. This could be: "The Software should also be capable of deleting products". Now we have a new use case in our system. This means we have to implement a new interactor with one or many new endpoint(s). It can also be that marketing want to extend a already existing use case. For example: "Products should also consider different taxes from different countries". Therefore we have to add a methods to our existing interfaces in our domain layer. Of course there are several ways to implement the use case, but I don't want to go into too much detail here. It is important that we know, where to add the use case and not how. Here we should have the open closed principle in mind.
As you can see, now we have a clear structure in our software. If a new requirement is requested, we already know where we to place it.
If you have made it this far, thank you for reading my article and feel free to leave a comment.
Code available at: https://github.com/code-specialist/product-creation-example
I hope you now have a better understanding of clean architecture. Finally, I would like to show you a quote by Robert C. Martin, which he wrote in his book Clean Architecture and of course therefore fits perfectly for Clean Architecture:
References
- Clean Architecture with Spring Boot - baeldung.com 11.11.2022
- The Clean Architecture - cleancoder.com 08.13.2012
- The Stable Abstraction Principle - objectmentor.com 10.02.2015
- SOLID Principles - code-specialist.com 15.03.2021
- AAA Pattern - medium.com 09.09.2017
- Meaningful Names - code-specialists.com 31.10.2020
- Mockito ArgumentCaptor - baeldung.com 05.01.2023