DEV Community

Blind Kai
Blind Kai

Posted on

Backend: Layered Architecture

Motivation

Choosing the correct architecture for your backend application is an important decision because you will get the most of it in a long shot. This article is written to give Backend Developers an understanding of how to structure the code in order to make it properly incapsulated correspondingly to its purpose. Properly structured code that follows the same rules for each inner domain is easier to be read and maintain for you and other developers that would work with it. Better code you write at the start - less pain you get later when requirements will change.

Considering Layered Architecture

When developers work in the backend, they've mostly worked with data and business logic around that data. No matter if you're dealing with API or if it's CLI - you need to structure your code properly.

Layered Architecture is a great example of lightweight architecture which is not overwhelmed with redundant abstractions, contracts or boilerplate code.

In order to implement this architecture correctly, you just need to place your code in a place according to its properties and purpose.

General schema

For example, we need to design REST API architecture. If you're interested in reading more about REST API design, please check this link.
The general schema of layers is shown below:

General schema for layered architecture

As you can see, we have three layers, let's discuss them in detail:

  1. Controller layer: Controllers themselves should only be responsible for work delegation. They should not contain any logic or direct data manipulation. The main purpose of a controller is to get the request, then pass it to the service that will process it and then return the response that is given from the service. See controllers like a manager for requests.
  2. Service layer: Generally services contain information that is related to their domain. For example, if we have "Mail Service" we expect that sending/receiving emails happens there like in a real life. The same is fair for the codebase. Services (and their methods) handle the business logic which means that they are responsible for transforming data, performing additional actions (like asking the repository for additional data or another service for processing some logic for it). So if we need to send a mail, the service is responsible for getting the data as parameters, formatting them into the mail itself and sending it thru an external service or saving it thru the repository if it's an internal functionality.
  3. Repository layer: If there is code that somehow works with the database here is the place for this code to live. No matter if we're fetching the data from the DB or saving/modifying objects, it's the place where SQL queries or ORM operations should be placed.

Detailed schema

Let's discuss a few workflows to have a better understanding of how things could work if we're building the REST API for some "Shop orders" domains.
Below you can see a few cases for writing domain code that follows the architecture:

Image description

Scenario A: User creates order

For example, we have a user that wants to create a new order. The request comes into the controller, the request is validated so we ensure that we're working with valid parameters.
Let's assume that the user also had discounts so we need to apply them in order to get the final price. For example, we have the service UserOrderService which has the method createOrder(user, order). Within this method, we calculate the discount that is available for the user (for example the user has a premium account or have a corporative discount so prices are lower). The calculations are the business logic so they live inside a service method calculateFinalPrice(order, discounts). After we are done with the logic, we could use UserOrderRepository with a method createOrder(order) which contains all fields like orderId, userId, creation date etc. Inside this method, we simply write our "INSERT" statement.

So we have a controller that passes the data into a service that process it and then the data is saved using the repository.

Scenario B: Confirm order, choose to ship and pay for the order

As you already see, here we have several things to work with. We have an order that should be confirmed, shipping options to be configured and money-related operations to be done.
So in practice, we get the request in our controller and pass it to the UserOrderService which handle logic that checks if the order can be confirmed with those parameters. After that, we're triggering services that have logic to work with external ShippingService and PaymentService. Those may contain some additional logic or utility methods that belong to them so we will need to implement some additional features for it. Finally, those services might trigger our repositories because we need to save that we need to pass the order to a shipping company and we know that the order is paid already.

Scenario C: User creates a comment below the bought product

The user got the order and want to give feedback (or post the comment on the product page). In this scenario, there is not much logic to work with the data (if comment moderation is manual. If it's done by AI it could have some logic in CommentService in method checkComment(comment)). If we have no logic there, we can simply pass the comment into CommentRepository which has a method called addComment(user, comment) and that's it. If no service is required you can simply trigger repository functionality from the controller.

Conclusion

Layered architecture is pretty simple and doesn't require developers to have big experience as well as it doesn't force developers to write too much boilerplate or deal with complicated abstractions and structures.
If you need to design MVP or if your project is not that big (yet) consider using layered architecture as a base architecture.

Discussion (0)