DEV Community

Kirill Birger
Kirill Birger

Posted on

Vertical patterns for organizing backend code

Synopsis

There is no shortage of articles prescribing and explaining the use of specific design patterns. Many of them do a great job. Rather than focusing on a specific design pattern, the the purpose of this article is to present a helpful way to organize your webservice or microservice code to maximize maintainability and testability. By having a structured way to think about code design, we can achieve much more with

I generally like to think of design patterns as belonging to one of two axi: horizontal and vertical.

All code can be organized in layers of concern. This is equally true for desktop or command line applications as well as web applications, although some layers can be different.

Horizontal patterns are those which generalize or abstract complexity across a single layer. Many web frameworks provide the concept of a controller. Controller is a pattern that is horizontal. That is, you may have several controllers, that perform the same sort of functionality across different areas of your application that are all concerned with HTTP.

Vertical design patterns abstract complexity across different layers of code, but for a single, specific area of concern. This may involve the way that a specific web service framework structures its stack of components to handle a single request from start to finish. For example, a request to your web service will usually cross the HTTP, Business logic, and data layers of your application. Most frameworks lay this out for the developer, but it is important to understand how to use what is provided effectively.

The reason I find this metaphor effective is because it works on a geometric level as well. In geometry, a vertical line is perpendicular to a vertical line, and thus has an intersection. Similarly, vertical patterns will intersect with horizontal patterns. There are many controllers, which form a horizontal "line", but each controller is also part of a vertical "line" for a set of given requests. When leveraged properly, you have well organized, maintainable, and testable code.

The following diagram lays out some horizontal and vertical patterns that are common in many application frameworks. It may be helpful to refer back to the diagram as parts of this article will cover some of the ideas that it lays out.

Image description

Lifecycle of a web request

This is a generalized view of a web request
Web Request Life Cycle

HTTP is a text-based protocol. When your web service receives a request, it is actually receiving a series of bytes that is decided into text. Different frameworks expose this in different ways, but for most use cases, we do not need to concern ourselves with how this happens. Nonetheless, for completeness, I will include it.

This leaves us with the following concerns, which happen at different stages:

  1. Deserialization: the bytes are deserialized into data structures that we deal with
  2. Cross-cutting handling of requests: usually implemented with something resembling a middleware.
  3. Request routing
  4. Business logic
  5. Error handling

Key Concepts

Single Responsibility

We will focus primarily on one of the SOLID principles. Namely, the Single Responsibility principle. I will not go into depth on this subject as it has been covered extensively. Briefly, as the name suggests, it is the idea that a class should have a single responsibility. In our case, we will be looking at common web service constructs and their responsibilities.

Vertical layers of a request and responsibilities

Web Layer / API Layer

This is the first layer activated by a request. It is responsible for handling HTTP complexity. It is the only layer which should be directly exposed to the representation of the request and response. This layer deals with request headers, query strings, encoding, response generation, etc.

It is the application boundary on the incoming side.

This layer must perform no business logic.

Application layer

This is the layer which contains all of the business logic for your application, and the associated complexity. You may structure code in this layer in the way that is most appropriate for the functionality you build, but it must not be exposed to any HTTP internals, and must not directly influence HTTP response construction.

A good rule of thumb is to ask yourself: could I put this code behind a different transport mechanism (such as a message bus, queue, or command line) without changing it?

It is important to have good error handling in this layer. Good error handling will explain the cause of the error, and usually provide some sort of machine readable mechanism that can be interpreted to distinguish classes of errors.

IO Layer

Most web services do not only perform computations, but also perform IO. For the purposes of this topic, it does not matter whether that IO takes the for of file system activity, HTTP, or database.

The IO layer should be completely unaware of the context of a web request, and should have no business logic in it. It serves as another application boundary.

Common Framework constructs

Different frameworks name these differently, and not all frameworks provide each of these. What is important is to understand the concepts and decide how to apply them.

Web Layer / API Layer

  1. Middleware and interceptors: cross-cutting constructs which can define operations to be applied in the Web layer for any number of endpoints. There are typically used for concerns such as logging, auth, caching, serialization, content type negotiation, and certain kinds of validation
  2. Error Filters or Error Handlers: these are a class of construct that are responsible for converting errors that bubble up from your code into HTTP responses. Sometimes they may be used for error logging, but it is preferable to do that in Middleware.
  3. Controllers and Route Handlers: these are responsible for mapping a request to an entry point to some business logic for handling the operation. If your framework does not provide other, more specialized constructs, they may be used to format a response, or convert errors to HTTP responses (if error filters are not available). Many frameworks provide an automatic way to tokenize the request route into parameters, parse query strings, and deserialize the body, so that this layer can receive these as data structures. If your framework does not do this for you, you should do so in the controller. Controllers should be kept as light as possible, to minimize testing effort. It is generally a good idea to let the controller call a single method or function and handle the HTTP complexity associated with its result.

Application business logic layer

Typically, frameworks do not provide anything for this layer. Many frameworks do provide a dependency injection framework to help orchestrate your business code.

As such, we will simply say that you should provide a "service" for your controller to call. In the context of DI a service is an implementation of business logic that can be instantiated by some reference to it, such as an injection token. This class can orchestrate any work necessary to perform a required operation.

IO Layer

Similarly to above, the only common touch point between your framework and the IO layer is the DI framework.

Summary

By splitting our code up in this fashion, and by creating well-defined interfaces between these layers, we are able to write code that is maintainable and easy to understand. It also becomes clear which patterns make sense to use in which parts of the code. To help in this task, we can also try to think of each layer as its own product, with its own contracts and supported features. By doing so, it forces us think more carefully about what we expose, and how.

Do you have other ways to split your code? Do think there are other patterns that are valuable to understand in this context? Please write in the comments below.

Top comments (0)