Is 3-tier Architecture anti-pattern?

In this topic, I would like to discuss about 3-tier architecture, what kind of drawbacks it has and what might be used instead of it.

spring-modulith and hexagonal architecture

Three-tier Architecture

Let’s talk about three-tier architecture. It’s a well-known pattern that defines in which way we should organise modules of our programs. The 3-tier architecture is very popular across web applications, but it might be applied to not only implementing this type of application. Based on the name, you might guess that there are 3 tiers:

  1. Presentation Layer
  2. Logic Layer
  3. Data Layer

Presentation Layer contains logic that is in charge of providing data via REST or other protocols. The code of this layer handles requests, takes needed data, transforms the data to required formats and sends responses. Usually, a framework is responsible for most of these actions but a programmer still needs to declare API and to define from which place data will be taken and where data should be passed to.
Logic Layer contains business logic e.g. creating new objects, changing values of existing domain objects and so on. It’s the most interesting part of the application.
The last one is the Data Layer. This layer provides access to a database in a convenient way. Due to the Data Layer, we don’t have to think about how to persist and retrieve objects from a database. Moreover, in theory, this abstraction tier lets switch database type without changing business logic e.g. from MySQL to PostgreSQL, or even to a NoSQL solution.
As a rule, each tier is represented by a package that contains all classes associated with a corresponding tier. Let’s look at an example. It is a simple content management system. The system provides a Web Interface and REST API to operate posts, comments, and users. The picture below depicts the structure of the 3-tier architecture of the application. For convenience, I mark the related classes with the same emojis.

.
└──ru
Β Β  └── izebit
Β Β          β”œβ”€β”€ ApplicationLauncher.java 
Β Β          β”œβ”€β”€ controller
Β Β          β”‚Β Β  β”œβ”€β”€ UserController.java πŸ‘½
Β Β          β”‚Β Β  β”œβ”€β”€ PostController.java πŸ—’οΈ
           β”‚   └── CommentController.java ✏️
Β Β          β”œβ”€β”€ repository
Β Β          β”‚Β Β  β”œβ”€β”€ UserRepository.java πŸ‘½
Β Β          β”‚Β Β  β”œβ”€β”€ PostRepository.java πŸ—’οΈ
Β Β          β”‚Β Β  β”œβ”€β”€ CommentRepository.java ✏️
Β Β          β”‚Β Β  └── model
Β Β          β”‚Β Β      β”œβ”€β”€ User.java πŸ‘½
Β Β          β”‚Β Β      β”œβ”€β”€ Post.java πŸ—’οΈ
Β Β          β”‚Β Β      └── Comment.java ✏️
Β Β          └── service
Β Β              β”œβ”€β”€ UserService.java πŸ‘½
Β Β              β”œβ”€β”€ AuthenticationService.java πŸ‘½
Β Β              β”œβ”€β”€ StopSpamService.java ✏️
Β Β              β”œβ”€β”€ CommentService.java ✏️
Β Β              β”œβ”€β”€ PostService.java πŸ—’οΈ
Β Β              β”œβ”€β”€ SpellCheckerService.java πŸ—’οΈ
Β Β              └── dto
Β Β                  β”œβ”€β”€ User.java πŸ‘½
Β Β                  β”œβ”€β”€ Post.java πŸ—’οΈ
Β Β                  └── Comment.java ✏️

As we can see, there are classes grouped in packaged by type, not by domain. All classes must be public because they should be accessible from other packages, otherwise, controllers can’t work with services and services can’t work with repositories.
Moreover, not related classes are also accessible to each other inside a package, for example, AuthenticationService and SpellCheckerService. In this way, based only on the structure, it’s tough to understand the relationship between classes. In addition, the current package structure contradicts the Encapsulation principle.

A language mechanism for restricting direct access to some of the object’s components.

  • Wikipedia

To restrict access to components from other packages, we might place all related classes into the same β€œbusiness logic” package and expose only classes that are used by other components. Other classes might have restricted modifiers e.g. package-private or private modifiers. This approach can be implemented with Hexagonal Architecture.

Hexagonal Architecture

hexagonal architecture

The backbone of Hexagonal Architecture is Ports and Adapters. Business logic has boundaries and is isolated from other components. There is no direct access to it from other packages.
Ports provide a shareable API that might be used to work with our business logic and vice versa i.e. to define API for interacting with the external world as well. Thus, it stands for β€œWHAT our system can do and other systems can do with”.
Adapters define in which way ports might be invoked and which way our system might work with other components. One port might have many adapters. In this way, adapter stands for β€œHOW the system and other systems interact each other”.
This approach lets us write maintainable code with isolated domain-specific logic. All dependencies are represented as interfaces and might be mocked if necessary. It makes writing tests easier. Also, it affects flexibility because we can change adapters without touching core logic. In other words, this design encourages Orthogonality.

Let’s look at the structure of the app with Hexagonal Architecture:

.
└── ru
    └── izebit
        β”œβ”€β”€ ApplicationLauncher.java
        β”œβ”€β”€ comment
        β”‚Β Β  β”œβ”€β”€ CommentServiceImpl.java
        β”‚Β Β  β”œβ”€β”€ StopSpamService.java
        β”‚Β Β  └── ports
        β”‚Β Β      β”œβ”€β”€ in
        β”‚Β Β      β”‚Β Β  β”œβ”€β”€ CommentService.java
        β”‚Β Β      β”‚Β Β  └── adapters
        β”‚Β Β      β”‚Β Β      └── CommentController.java
        β”‚Β Β      β”œβ”€β”€ models
        β”‚Β Β      β”‚Β Β  └── Comment.java
        β”‚Β Β      └── out
        β”‚Β Β          β”œβ”€β”€ CommentRepository.java
        β”‚Β Β          β”œβ”€β”€ UserService.java
        β”‚Β Β          └── adapters
        β”‚Β Β              β”œβ”€β”€ CommentRepositoryImpl.java
        β”‚Β Β              └── UserServiceImpl.java
        β”œβ”€β”€ post
        β”‚Β Β  β”œβ”€β”€ PostServiceImpl.java
        β”‚Β Β  β”œβ”€β”€ SpellCheckerService.java
        β”‚Β Β  └── ports
        β”‚Β Β      β”œβ”€β”€ in
        β”‚Β Β      β”‚Β Β  β”œβ”€β”€ PostService.java
        β”‚Β Β      β”‚Β Β  └── adapters
        β”‚Β Β      β”‚Β Β      └── PostController.java
        β”‚Β Β      β”œβ”€β”€ models
        β”‚Β Β      β”‚Β Β  └── Post.java
        β”‚Β Β      └── out
        β”‚Β Β          β”œβ”€β”€ PostRepository.java
        β”‚Β Β          β”œβ”€β”€ UserService.java
        β”‚Β Β          └── adapters
        β”‚Β Β              β”œβ”€β”€ PostRepositoryImpl.java
        β”‚Β Β              └── UserServiceImpl.java
        └── user
            β”œβ”€β”€ AuthenticationService.java
            β”œβ”€β”€ UserServiceImpl.java
            └── ports
                β”œβ”€β”€ in
                β”‚Β Β  β”œβ”€β”€ UserService.java
                β”‚Β Β  └── adapters
                β”‚Β Β      └── UserController.java
                β”œβ”€β”€ models
                β”‚Β Β  └── User.java
                └── out
                    β”œβ”€β”€ UserRepository.java
                    └── adapters
                        └── UserRepositoryImpl.java

In comparison to the previous one, this structure brings a few additional interfaces and packages, but at the same time, it gives more information on the relationships between components.
The protected package visibility modifier hides all domain-specific classes. Only components of packages */ports/in/ and */ports/model/ must be public i.e. accessible from other packages. By now, there’s still no clear relationship between packages. We need to provide public access to only particular packages.
For instance, during working with comments and posts, we need to check users if they are authenticated, they present in blacklist etc. It means that classes from the post and comment packages should have access to public classes from the user package, but the reverse is false.
Limiting access might be implemented with Java Modules, but we discover another approach.

Spring Modulith

This Spring component lets developers implement logical modules in Spring Boot applications. It supports structural validation, generation of Module Component diagrams etc. In this post, we are going to use a few modules of this Spring component.

First of all, we need to mark our public packages with the NamedInterface annotation.
By default, all top-level packages are considered as modules and there is no need to define them explicitly. We do it only for sub-packages.

@org.springframework.modulith.NamedInterface("in")  
package ru.izebit.user.ports.in;

Then we should define dependencies for each package. For this purpose, we use the following annotation:

@org.springframework.modulith.ApplicationModule(  
        allowedDependencies = "user::in"  
)  
package ru.izebit.comment;

This information on packages are stored in package-info.java files placed in the relevant directories.

After these two steps, we can write a simple test that checks correct packages access.

@Test  
void shouldBeCompliant() {  
    ApplicationModules.of(ApplicationLauncher.class)
				      .verify();  
}

If access is broken, we can see the following output:

org.springframework.modulith.core.Violations: - Module 'comment' depends on module 'user' via ru.izebit.comment.ports.in.adapters.CommentController -> ru.izebit.user.ports.out.UserRepository. Allowed targets: user::in.

The error message is descriptive and gives enough information to find out the root of the issue.
In addition to checking package access violations, one of the features of Spring Modulith is generation of UML diagrams. If you make the code below a part of build pipeline, you will always have up-date diagrams of your system.

void writeDocumentationSnippets() {  
    new Documenter(modules)  
            .writeModuleCanvases()  
            .writeModulesAsPlantUml()  
            .writeIndividualModulesAsPlantUml();  
}

c4 uml diagram

The picture depicts C4 PlantUML diagram of the application.

You can find the source code of the application here. project on GitHub