Multitier Software Architecture
In this article we will examine an approach of layering back-end applications. The examples are given in Java and Spring Boot but the approach is language and framework agnostic. It can be applied to any backend application.
Separation of concerns
One motivation behind layering an application comes from the need to assign responsibility to specific components of the complete system that an application forms.
Intuitively, rather than viewing the entire system as a single entity, it is easier to picture a full application flow composed of individual layers, where each layer has its dedicated role.
Spring Boot
Spring ecosystem makes it easier to write enterprise grade applications using well defined design patterns. Spring Boot framework takes it further and enables developers to write enterprise grade applications with less code (via auto-configuration, annotations, IoC, and other means).
Scenario in practice
We can examine multitier architecture in a practical scenario of building an authentication service that is meant for users to authenticate with our backend.
Let’s look at the layers from a high level.
Controller (Contract layer)
Controller layer is essentially the ‘entry-point’ layer to an application. It is conventionally known as an API. From a functional standpoint, that layer defines a contract, specifically, how external or internal services interact with the application.
In Spring Boot, a “stereotype” for controller classes is known as @Controller
or @RestController
. The latter is a more specialized annotation to define a controller class that relies on the REST (http/https) protocol for communication.
Controller — scenario in practice
Let’s examine our Controller layer class for authenticating with the backend.
Authentication typically requires some type of login functionality, so our Controller has a contract that defines it— a function called “login” that is exposed at the endpoint/login
and consumes LoginCredentials
data transfer object via a POST request.
In the Controller layer we inject the Service layer class calledLoginService
via @Autowired
annotation. LoginService
is the downstream layer that we will examine next.
Service (Business layer)
Service layer should generally not be exposed externally but rather encapsulated within the application. Controller layer indirectly exposes the Service layer by defining the correct contract.
Service layer is responsible for defining the business logic of an application. It often is the most complex layer when we look at the code, i.e. the brain of the application.
Service — scenario in practice
In the scenario of authentication we need logic for validating user’s credentials against some persisted state to ensure that a user is allowed to access our application. Details of how that (business) logic is implemented is the responsibility of the Service layer.
Our LoginService
class defines a method called authenticate
that validates a user’s access credentials, and upon successful validation generates an access token that is returned back to the client, or throws BadCredentialsException
otherwise.
In practice, you would have LoginService
use a production-grade authentication protocol library (for example, “OAauth2”) to validate credentials and generate an access token, but for the purpose of this article we simplified that logic.
In the Service layer we inject the Repository layer interface UserRepository
via @Autowired
annotation. UserRepository
is the next downstream layer.
Repository (Persistence contract layer)
One way to think of the Repository layer is, an abstraction of how the application communicates with the persisted state (or storage).
Repository is the contract of how the Service layer interacts with the storage. We can call it a persistence contract, although, it is also known as data access layer.
Repository — scenario in practice
UserRepository
interface defines a method called findOneByUsername
which queries the (downstream) Persistence layer for a specific object (`User`) by the provided field (`username`).
We use Spring Data JPA repository implementation which defines “finder” methods that Spring framework automatically translates to the underlying storage backend language. This removes the need for writing boilerplate storage-specific code.
The power of such implementation is that you can apply the same abstraction to multiple storage backends (PostgreSQL, MySQL, Oracle DB, etc.).
Persistence (Domain layer)
The final layer (of the application flow) is the Persistence layer, also known as the Domain layer.
The responsibility of this layer is to define data objects of the application. Those objects can be viewed as the building blocks of the information that our application persists.
Domain layer also defines relationships (and constraints) between those objects and the types of information that those objects store.
Persistence — scenario in practice
In our authentication service example, we need to store information about the primary entity that authenticates with the application — a user.
User
is the Domain layer class that stores information about a user. It stores 4 fields — id
, username
, password
, and email
. It defines a type for each of those fields, as well as certain constraints. For example, username
should be unique and it cannot be null
.
User
object is queried in the Service layer and retrieved via the Repository layer. Service layer needs Domain objects for the business logic implementation. In our scenario, it needs to validate that the user has access to our application.
Configuration layer
Configuration layer can be viewed as a supporting layer in the application flow, and is an independent layer — meaning, it should not depend on any of the other layers. Its purpose is to store both variable and static configuration of the application. Configuration layer is implicitly or directly used in the Controller, Service, and Repository layers.
In Spring Boot, configuration is typically defined in .properties
or .yaml
files, and exposed to the code via @Configuration
stereotype. In practice, there are multiple @Configuration
classes which define the Configuration layer.
Configuration — scenario in practice
In our authentication service we define the Repository (and the Persistence) layer’s configuration via a dedicated block in application.yaml
file - specifying database platform to use within our JPA repository and domain objects.
In the code we define dedicated@Configuration
class specifying package paths to the domain classes (via @EntityScan
annotation) and to the repository interfaces (via @EnableJpaRepositories
annotation).
Source code for this article is here.