Promoting SoC Through Application Layering

Creating a layered design in an application is a fundamental element of modern software architecture. The goal is to promote the Separation of Concerns (SoC) design principle.

Separation of Concerns

The ideas behind SoC date back to Dijkstra's 1974 paper "On the role of scientific thought".

In computer science, separation of concerns (sometimes abbreviated as SoC) is a design principle for separating a computer program into distinct sections. Each section addresses a separate concern, a set of information that affects the code of a computer program. A concern can be as general as "the details of the hardware for an application", or as specific as "the name of which class to instantiate". A program that embodies SoC well is called a modular program. Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface.

- Wikipedia

SoC is a broad design principal that can be applied to all aspects of software development. It can be applied to everything from how objects are designed to how services are defined to how a datastore is layed out. It can be difficult to properly apply the SoC principle to an application and there are many factors to consider:

  • How diverse is the application? Does the application address a single problem/task or is it handling hundreds/thousands of tasks?
  • How large is the audience for the application? Does it need to be highly scalable?
  • Are there future plans to expand the application?
  • How many subsystems will the application be interfacing with?

These are some of the issues that need to be considered when designing an application. The larger and more diverse the application, the more layers of abstraction it makes sense to have. But there is an important balance to maintain... too many layers and your application can become an unmaintainable mess of interfaces and abstractions (aka the Lasagna Code anti-pattern). Too few layers and every element of the application becomes tightly coupled and immutable, impossible to change without breaking other areas of the application.

Application Layers

SoC is a broad design principle, we're focusing on how to apply it when designing the layers in an application. Fortunately, there are some simple ways to incorporate SoC into an architecture that are proven and well-defined. A common implementation involves splitting the application by technical and business functionality. A common scenario is to have a Presentation layer, a Business Logic layer, a Persistence layer, and a Database layer.

Layers in a Single Project

Before examining an entire application, let's examine a single WebAPI project. A simple way to create some basic layers and promote SoC is through code and namespace organization. We can design a project that can easily be scaled in the future; each layer can be pulled out and refactored as needed.

Simple Application Layers Diagram

Let's look at how the diagram above translates to a real WebAPI project in Visual Studio.

Visual Studio Screenshot Showing Layers

You can see that each theoretical layer corresponds to a folder/namespace in the single project. Each layer can now be refactored independently, and because interfaces and dependency injection are used, the actual implementation of each layer's logic can be easily swapped out.

Layers in an Application

Let's zoom out now and look at a slightly more complex scenario where we need to create layers across multiple projects. We'll create business and technical layers, represented by horizontal and vertical sections in the diagram below.

Application Layers Diagram

For simplicity, most of the project names match the layer definitions. The Perisistence layer has a corresponding Persistence project, the Business Logic layer has a corresponding BusinessLogic project, and the Datastore layer has a corresponding Data project. The Presentation layer is represented by the ApplicationLayersDemo project. In a real application, the project names should be meaningful and specific to the application.

Screenshot of Visual Studio Application Structure

Working from the bottom up, the Persistence layer abstracts out the Datastore layer, meaning that code calling the Persistence layer doesn't care if the data is stored in a local DB or remote Web API. Additionally, the Business Logic layer abstracts away the Persistence layer, so Presentation code that calls the Business Logic does not care about how the Persistence code works. In this example, switching between a DB and a Web API as the Datastore is a trivial change to the Peristence layer and has no impact on the Presentation and Business Logic layers.

Conclusion

Using some very simple and fundamental SoC techniques, we can create an application that can be easily modified and scaled. In the fast moving world of application development (Agile), where immediate results are a critical part of each cycle, these are some fairly simple design patterns that will reduce technical debt without generating too much overhead. Just use caution not to add unnecessary layers or abstractions, always try to keep it as simple as possible.

For further reading, refer to:

This example was created using Visual Studio 2022 and .NET 8 running a Blazor Web App project, Class Library projects, and an ASP.NET Core Web API project.

Source code: https://github.com/jharrell-bits/ApplicationLayersDemo