Have our layers gathered dust, for instance?
Architectural styles such as Clean Architecture by Robert “Uncle Bob” Martin or Hexagonal Architecture by Alistair Cockburn have attracted increasing interest in recent years. They aim at being able to develop and test the technical core of an application independently of frameworks, databases and other infrastructure. In addition to improved testability and maintainability, this approach also allows infrastructure components to be defined at a late stage and easily replaced by others at a later date. The article explains the disadvantages of traditional three-tier architectures and the improvements that can be achieved with Clean Architecture.
The traditional three-tier architecture
In the following, “layers” always refers to logical layers, not the typical physical tiers of client, server, and database. In this logical sense a three-layer architecture, as it is surely still used in a multiplicity of systems, looks as follows (on the left only the layers and their dependencies are represented, on the right exemplary classes in these layers):
The presentation layer (Presentation oder User Interface Layer) is responsible for presenting the application to the user. It interprets the user’s input and calls the corresponding functionality in the business logic layer (often called Business Logic or Domain Layer). Usually, this functionality requires evaluating or changing data that is persistently stored in a database. For this purpose, the Business Logic layer calls a Data Access Layer, which handles the necessary communication with the database.
The basic principle of layered architectures is that the components of a layer may depend only on components in layers below it (often even only on the layer immediately below it), while they know nothing at all about the layers above them. The higher layers can therefore be easily replaced or changed without having to adapt the layers further down.
The layering itself is not problematic, but the traditional manifestation of the three-layer architecture brings the following disadvantages:
- Everything depends on the data access layer, and that in turn depends on the database schema. Ultimately, therefore, everything revolves around the database first and foremost. While all database vendors surely see this as the natural order of things, in times of agile development, possibly even in combination with Domain-Driven Design (DDD), this is hardly practical. After all, it means (at least without further efforts at decoupling, see below) that changes to the application that affect persistent data are made first in the database and then propagated upward through the layers. Because databases are cumbersome and costly to test, people prefer to do it “right” the first time and / or resist optimizing a moderately fit design for new requirements later, unless they absolutely have to. Both behaviors (“big design up front” and “never touch a running system”) are notorious impediments to progress from the days of the waterfall model.
- If the business logic layer directly calls the data access layer and the data access layer directly calls the database, not even the business logic can be tested independently of the database. In any case, this makes test setup costly and test execution slow. If you don’t bother to create a precisely defined initial state in the database before each test, but only make sure that certain data is present and certain others are not, the test result is also less predictable.
- The business logic layer contains the business core of the application (in his book Clean Architecture, Uncle Bob calls this the “policy,” or business rules), which is actually highly abstracted from technical details such as databases and web frameworks. However, because of its dependence on the very technical data access layer, the business logic layer must also be adapted every time technical details of data access change.
- Although the business logic layer is in principle independent of the presentation layer, many GUI frameworks tempt developers to place small or even large parts of the business logic in classes such as controllers, which are required by the GUI framework and are therefore located in the presentation layer. This runs counter to the Separation of Concerns intended by the layered architecture.
- To make matters worse, the introductory documentation of many GUI (and also other) frameworks focuses on explaining the concepts of the framework as simply as possible with the shortest possible code examples and neglects architectural aspects for this. There e.g. in a GUI class also times fast a few technical computations and perhaps even DB accesses are accomplished. With the DB accesses it is clear to most developers that that must be separated in production code more cleanly, with the technical computations however not so necessarily.
Decoupling by Dependency Inversion
In the “pure form” described above the three-layer architecture is hardly still used due to the many disadvantages with new systems, and also older systems are reorganized before lining up changes at least in the parts concerned by means of Refactoring often so far that the Business logic is not coupled any longer directly with the data base. To do this, one first applies the Dependency Inversion Principle (DIP) by extracting an interface so that the business logic in the future depends only on this interface, not on the concrete implementation, which always calls the database right away:
In unit tests for the corresponding parts of the business logic, one can now implement the interface by a test double (e.g. a mock object) that provides exactly the answers needed for the test without calling a database. Of course, such tests run much faster than tests with a database and are also much more controllable, because you have everything under control.
What about dependency at the architecture level?
By using the DIP, we have achieved that
MyService no longer depends directly on
MyDataAccessObject, which has improved testability tremendously. However, from the architecture point of view (left side of the diagram above) nothing has changed: The domain-oriented business logic still depends on the technical component for data access. If the new interface is created purely as an implementation artifice to improve testability, but is still aligned with the technical requirements of data access and located in the data access layer, the problem also remains that the business logic must always be adapted to technically motivated changes in the data access layer.
To solve this problem, one has to rethink a bit more: Not the data access layer should provide a low-level interface to access the database, but the business logic should provide a technically motivated high-level interface, which has to be implemented by a persistence mechanism appropriate to the technicality! The business problems that a typical enterprise application solves usually have nothing to do with manipulating or finding certain records in a database table, so you won’t find anything like that in such an interface. A business logic component for dunning, however, will have a need to find unpaid invoices that would have been due by a certain date, for example, and may require a method like
Collection<Invoice> findUnpaidInvoicesDueBy(Date dueDate) to return just those invoices.
Uncle Bob refers to such an interface as a gateway or gateway interface. Eric Evans coined the term repository for the analogous construct in Domain-Driven Design. The idea is similar, but the term gateway emphasizes access over a network, while the term repository downplays this aspect. Since the primary purpose here is to describe Clean Architecture, we will stick with Gateway:
As you can see, the architecture-level dependency now goes from data access to business logic, making business logic independent of technical details of data access!
Since the capabilities required by the gateway implementation are business-defined, no technical details can appear in the gateway interface that apply only to a particular persistence mechanism. Accordingly, it should be easy to create an implementation for PostgreSQL or even non-relational databases like MongoDB in addition to an implementation for – say – Oracle. This makes it comparatively easy to change database technology (or even just the OR mapper, if one is used for implementation) later, or to support multiple databases for the same application (say, for different customer installations).
With this architecture, it is even possible to implement and test the business logic (or a significant part of it, e.g. for a Minimum Viable Product) already when you don’t even have a database running yet and you haven’t even decided on a specific database! This allows you to postpone this technical decision until you know enough about the actual requirements for a successful product to evaluate the pros and cons of particular database technologies with respect to those requirements.
Is there anything to consider for the GUI as well?
As seen in the last figure, both GUI and data access components are now dependent on the business logic, while the business logic is independent of either. To guarantee this for the GUI as well, Clean Architecture recommends first developing the business logic not only independently from the database, but also independently from the GUI and related frameworks, purely based on business-defined use cases driven by tests (TDD - Test Driven Development).
The use case implementation specifies an interface (including the data to be supplied), which the GUI (or a unit test) can use to trigger the use case. The use case in turn calls the corresponding domain-oriented logic and returns the result in a domain-oriented format. Providing the user with suitable feedback is then again the task of the GUI and not part of the business logic.
This approach ensures that the business logic is not dependent on the GUI framework used, so that this framework can be replaced relatively easily without having to change the business logic. This also makes it much easier to support multiple GUI clients in parallel (e.g. native clients for Android and iOS in addition to a web client).
The resulting architecture style “Clean Architecture”, where the business logic as the domain-oriented core of the application is developed and tested independently of all I/O details and frameworks, is preferably represented in the form of concentric rings - here there is no longer “further up” (closer to the GUI) and “further down” (closer to the database) as in traditional layered architectures, but “further in” (closer to the domain-oriented core) and “further out” (closer to the technical details):
Here, the business logic is once again divided into the inner business core (“Entities”, yellow), which is determined “application-neutrally” purely by the business environment of the company, and the concrete implementation of associated use cases in the current application (“Use Cases”, red) - this also roughly corresponds to the usual division in DDD into “Domain Model” and “Application” (in any case, this is a useful simplification if you want to use both together). The next ring (“Interface Adapters”, green) contains adapters that convert between the data formats most suitable for use cases and entities and the formats required by the frameworks and drivers of the outermost ring (“Frameworks & Drivers”, blue). In addition to technical frameworks, the outermost ring usually only contains glue code that connects the frameworks of the blue ring with the interface adapters of the green ring.
The “dependency rule”, indicated by the arrows between the rings, is particularly important here: Code dependencies may only go from the outside to the inside, but never from the inside to the outside. So that esp. the technical core remains untroubled by technical interests, it may also know nothing of these technical interests!
Uncle Bob has published his own summary of this architectural style on the Clean Coder Blog.
What can Clean Architecture be used for?
It is clear that decoupling the rings of Clean Architecture as described above not only brings a lot of benefits, but also some work. This effort is worthwhile if the functionality to be implemented is rather complex and will probably have to be maintained over a longer period of time because it supports important business processes. For simple CRUD applications (which usually only need reasonably nice masks for direct editing of database tables) this is of course not worthwhile, perhaps a low-code platform or Microsoft Access will do. For disposable prototypes and short-lived demonstrators, the effort is of course not worthwhile, since the Clean Architecture only shows its advantages when business or technical changes are pending, which then only affect a small, well-defined area of the application.
By the way, this recommendation for use coincides quite well with the relevant recommendations for the use of Domain-Driven Design, which is why the two complement each other very well. For Microservices and Self-Contained Systems this combination is especially worthwhile, because DDD has a very good construct for domain-oriented cutting of Microservices (or Self-Contained Systems) with the Bounded Context and Clean Architecture helps to develop the domain-orientedness of a service independently from other services, even if it has to interact with the other services at runtime.
A good (especially not absolutely trivial) example application that takes the principles of Clean Architecture (and also Domain-Driven Design) to heart is, for example, Microsoft’s eShopOnWeb for ASP.NET Core, which is also available in a Microservices variant as eShopOnContainers.