Approach to Clean Architecture in Angular Applications — Hands-on
After we have seen in theory how a web application project can be structured according to Clean Architecture, let us see how we can implement this pattern in practice. If you have missed the introduction article, then you can find it here. We will go through all layers and see what’s implemented there. You will find the whole code here.
The sample application is a birthday calendar for elephants. It is kept very simple to clarify the usage of Clean Architecture. It takes data from an API or a MockRepository included within the app and displays all Elephants and their birthdays in a table.
An Angular project could be structured in the following way, starting off with the known structure generated by the angular-cli.
At first, let us have a look at our Core layer. As we know, we should define our core entities, usecases, repository interfaces, and mappers here. To keep the architecture clean and reusable, consider adding inheritance for the usecases and mappers.
The entities of this application are kept very simple, so an ElephantModel contains the elephant's name, its family status (mother, father, baby…) and a Date for its birthday. These are all information that’s our core application needs.
Usecase and Mapper Base
A Typescript interface is sufficient to keep the mapping process of all entities through the whole project consistent. It has only two functions, one to map from the core entity layer, and one to map to the core entity layer.
The usecase consists of one main function, that is called when we run our usecase and returns a RxJS observable. We also define an input parameter S to pass parameters during the usecase execution.
Repository Base and ElephantRepository
Read and write operations are handled in this application through repositories. Please notice, that only the interfaces are specified there for each repository and that a repository interface does not have to be an actual repository. This means that these interfaces do not need to talk to a relational database or NoSQL store, but to a restful API for instance. As already mentioned earlier, using repository interfaces for querying APIs is a perfect fit, because a lot of APIs are based on CRUD operations which can be perfectly represented as a repository.
Our actual interface for our simple elephants birthday API provides queries to search for an elephant by its ID and list all elephants which are stored in the repository.
Note: Because we will later use this class as a base class for dependency injection with Angular, our repository must be an abstract class. This is caused by Typescript, that does not know interfaces at runtime and dependency injection will fail. Interfaces in Typescript are just present in static code checking but are removed during compiling.
Finally, let us have a look at the core of our architecture pattern — the usecases. In our sample application, our usecases more or less duplicate the functionality of the repository but adding some level of abstraction in between. Remember, due to the dependency rule, usecases can only use layers which are lower than their current layer — in this case, that’s very easy because we only have our core layer and nothing underneath.
But does our usecase need to know where it can find the data? Not necessarily. The only way the usecase can talk to the data source is through the repository interface, which we will provide as a dependency like this:
Note: As an attentive reader you may wonder why there is an Angular annotation on a core layer where theoretically should only be plain Typescript without any external dependencies to frameworks. The reason is, that Angular only has this @Injectable annotation to provide a module via dependency injection. As you may remember, we talked about a fourth layer that was called configuration. If Angular would have such a functionality like Spring Boot or Dagger does, then the configuration layer could take care of our dependency injection. But for now, we have to stick to this solution as long as we do not want to hack the dependency injection mechanism.
Moving on to the data layer, we start implementing the actual repository. To show the usage of the Clean Architecture approach, we implement the repository twice. First, a mock repository, secondly with a REST client that talks to a small API hosted mockAPI.
Both implementations are very similar and consist of three classes:
- The actual repository that implements the repository interface defined in the core layer
- The other elephant entity that the repository needs for managing and working with the data on database or API level
- A mapper that translates the data objects from data layer representation into core entity representation and back
This approach creates some overhead through the duplicate code and might seem a bit weird at first. Nevertheless, decoupling business logic entities, data layer entities and presentation layer entities can be very useful, because they often have different or additional fields caused by their usage.
There are a lot of scenarios in which the abstraction layer can be handy. In our application, the API, for example, is delivering the birthday of an elephant as milliseconds, but our core logic or data structure is more convenient and suitable with the Date format, so using one entity for both could be problematic. So our mapper simply converts the time formats back and forth.
The repository implementation uses the standard Angular http client and maps the entities from the API format with the help of the mapper class into our applications core entity format.
Since we are now finished with our core business logic, usecases and the repository implementations, our application is ready to run — we just have to show that the application works. This is handled by the presentation layer. This layer can be as Angular as you want because you only make use of all underlying layers and call the usecases here. Since we defined our repositories as injectables, our usescases automatically know where to search for the right repository and— in addition — the repository can be easily exchanged through our interface!
But how does Angular know which repository we want to use? As we saw, we have two repositories implemented in this project — mock and web. We simply have to add one line to the module where we want to provide:
The “useClass” parameter offers the possibility to specify any class that implements the ElephantRepository interface. So just replace “ElephantMockRepository” with “ElephantWebRepository” and our app is ready to go online!
The rest of the presentation layer is really straightforward: Each component that has to access our elephant's list needs to get the right usecase injected and is ready to use the business logic just like it would use a service. Theoretically, the presentation layer should also have its own entity classes to show data on the UI.
Finally, I want to sum up with the benefits and drawbacks that Clean Architecture has to offer:
- The architecture is “clean” and “screaming”: You can easily see what is going on in the project by looking at the usecase folder.
- Separation of concerns: Each application layer has its own responsibility, so functionality and framework logic is mixed up.
- Modularity: Since all core business logic is represented by interfaces (or also called contracts), implementation details are not part of the actual business logic. That is why the data is stored can simply be exchanged.
- Portability: The core module is written in pure Typescript, so theoretically the core logic could be transferred to other applications to share the same code base (e.g. backend).
- Testability: Even if this article did not (yet) covered testing, you can imagine that testing mockable interfaces and usecases without side effects is much more convenient.
- Universally applicable: Once the basic concepts of Clean Architecture are understood, it could be applied to any kind of application, ranging from backend to mobile or — as we just have seen — web development. Once all projects are equally structured, it is much easier to get onboarded.
- Overhead: Separation and modularity are purchased in expense of more classes and interfaces.
- Duplication of code: Using different entities on each layer is automatically some kind of code duplication. On the one hand, decoupling is great to offer a layer of abstraction, but on the other hand, a new source of failure is introduced.
- Steep learning curve: For beginners, Clean Architecture does things very different than known form the actual framework.
I hope you enjoyed my little introduction into the world of Clean Architecture and that it helps you at least with the last mentioned point in cons: Minimize the learning curve! 😊