What is Divine architecture and Why you don’t need it

iamprovidence
11 min readMay 7, 2024

--

Have you ever wondered what architecture is? You may be familiar with a few popular ones like N-layered, Onion, Hexagon, etc. You may even apply one of them in practice. However, not many people know that everybody can invite their own architecture 😁.

Today, we will start with the simplest project and design our own architecture. We will go through discovering each layer, their purposes, and the dependencies between those. You will see how to build the best architecture. But most importantly, why you don’t even need one.

Are you ready? Let’s start our quest for Holy Grail🤠

What/Why/When architecture?

❓ First of all, let’s make sure we are on the same page and the term is clear for us.

Software architecture — is components and dependencies between those.

In a small project, components are just classes. In monolithic, usually by components, we mean separate DLLs. While for distributed systems — separate microservices.

Regardless of the scale, it is preferable for dependencies to satisfy the following conditions:

  • component should depend only on the required components
  • dependencies should point out in the form of an acyclic graph or a tree, but not be circular

❓ Now, when you are in a loop, it is time to answer why we even need to bother about architecture.

There are many reasons. Pick any you like to determine your destiny:

  • clear separation of concern isolates specific functionalities making it easier to understand, maintain, test, and modify each part of the system without affecting the others (you are an architect 👷‍♂️)
  • a well-known architecture provides a set of rules that can be applied across different projects. It makes it easier for developers to understand new projects, their components, what those contain, and how to use them (you are a team leader ️🦸‍♂️)
  • by identifying and eliminating unnecessary complexities, architecture can streamline the development process and improve the project’s cost-effectiveness (you are a project owner️️👨‍🎨)

❓ Surely not every project needs deeply-thought architecture. Some do not need it at all. For others, MVC-like separation would be enough. However, most enterprise projects grow in size and complexity, and it is essential to organize the codebase in a structured manner.

So, let’s learn how to design architecture properly.

How architecture?

It is a rarity to start a new project from scratch. Most of the time you will be participating in an already working environment without a need to reinvent the wheel. Often next architectural layers can be found:

Developers are so used to them, that they barely question those layers. Why only four layers? Can we have more? Less? Different layers?

In practice, the are reasons for each layer. You will see it yourself.

Trying to find an architecture supreme

Let’s say we start a new application. Nothing overcomplicated, just a simple website for an online store. In the beginning, it was just one executable project. As time goes on, we add new features and functionalities. Quickly enough project gets more complicated. Not only that, it gets more messed up.

First, you start grouping classes by folders. Then, you run out of names and introduce directories without clear responsibilities like Common, Shared, Data, etc.

As the tendency shows, without clear separation, it becomes impossible to add new features and maintain the project.

Since we are aiming to create a long-living application, we will start by designing architecture and all its components.

The executable project that contains Main() function is called composition root. It composes and registers all dependencies and modules together.

Not only that, Main() function starts your project and makes it available for interaction. This is our first layer —the user interface.

We will leave this layer for now and get back to it later. So far that is all you need to know about it.

Business Logic

An essential and fundamental layer that any application requires is Business Logic.

It represents concepts and rules of the business. Our domain area. Those exist even without software.

In our case, we are talking about a store. The final price of an order will be calculated as the sum of each product multiplied by its quantity.

From this single sentence, we can extract business entities like order, product, and business behavior like calculating the sum of the product. Entities can be represented with classes when behavior with algorithms.

The day you decide to go online and automate your work with software, those would be the first stuff to design. Not your UI, not tables in DB. The very first thing you should focus on is business logic, its concepts, and rules.

In a business logic layer, we can place the next stuff:

  • entities and enums
  • services and helpers
  • business-related events/exceptions/dtos and so on

Entities represent concepts of our business:

class Order
{
public DateTime CreateDate { get; set; }
public OrderStatus Status { get; set; }

public ICollection<OrderItem> Items { get; set; }
}

class OrderItem
{
public Product Product { get; set; }
public int Quantity { get; set; }
}

class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}

Business rules, like the calculation of the order price, could be placed in business logic services.

class OrderDomainService : IOrderDomainService
{
public decimal GetTotalPrice(Order order)
{
var totalPrice = order
.Items
.Sum(x => x.Quantity * x.Product.Price);

return totalPrice;
}
}

Basically, anything that is written in the requirements and described by our domain experts will be in this layer.

If you have heard about such things as DDD you can place here other stuff like aggregates, events, factories, repositories, and so on.

However, there is still one problem with this layer. Right now, nothing stops inexperienced developers from using IOrderDomainService in the Order entity. To prevent that from happening, we can split this module even more.

The difference between a separate project and a folder is that a project allows you to manage and control dependencies.

This time components only depend on what they need and one class can not call another in the wrong way.

This is what differentiates good architecture from bad one. Good architecture prevents you from making mistakes.

Notice, because a project with business services implementation reference interfaces implicitly has access to entities.

Just one last remark. The name for this layer may be misleading. Let’s say your application is Paint. It is not related to business and does not bring any money. Without software, it is just a drawing on paper. Nevertheless, you still can define entities like brush, pencil, rectangle, etc. The business layer is not about business it is about domain area. Therefore this layer is often called Domain.

User Interface

Business logic is great, but it does not do much by itself. Users are not developers, nor business logic experts. They don’t care how the order is calculated, all they care about is pressing some buttons and seeing the result.

That is why we get back to the User Interface.

User-Interface is responsible for interaction with users. Users can be different. It could be real people👨‍🦱 or automated bots 🤖. Users may have different experiences. Some are used to buttons, while others like us, developers, prefer console. My point is, that the interface can and will be different.

You already can see the benefit of extracting the business layer. It does not only clearly separate concepts but also allows you to reuse the code in case you have multiple interface layers.

In this layer, we can define interface-specific stuff:

  • for CLI it will be parsing console arguments, rendering to the console
  • for GUI it may be WinForms, Controls, DataGrid, etc
  • for API it is usually Controllers, Middlewares, Filters

One may argue, that it is better to start designing your application from UI. Customers are not developers. They won’t understand architecture. However, they do understand UI. Sometimes you can start from UI and show it to customers. This way it is easier to gather requirements and apply changes.

The problem with this approach is that if you start from UI, you may completely forget about business logic 🙃.

Application

Even though we have extracted business logic, at some point you will notice, that you still have duplicated code across different UI layers. No wonder, you can create an order from the GUI, it should also be possible from the API. This is what our application is doing.

This logic can be moved to a new Application layer.

For this layer to be reused it has to be UI-agnostic and does not reference anything ASP, WPF, WinForm related.

Even if you have one interface, keep in mind we are building Divine architecture here and trying to predict all possible outcomes 😁.

Jokes aside, there is another practical reason for this layer to exist. It not only orchestrates business logic. You can move software (application) behavior there:

class CreateOrderDto
{
public Address Address { get; set; }

public List<OrderItemDTO> OrderItems { get; set; }
}

class OrderApplicationService
{
public void CreateOrder(CreateOrderDto order)
{
. . .
}
}

And, of course, let’s not forget about the unit test 😁.

Infrastructure

The last missing part is all external dependencies like database, emails, monitoring, networking, event bus, blob storage, etc. They don’t fit in any layer we have so far.

Therefore we need to add a new one — Infrastructure.

We need a proper place for this layer on our diagram.

Our Business logic is just entities and pure rules. It does not need to store anything in a database or send messages to an event bus.

Our Presentation layer also does not have any logic. It just displays shiny buttons and fancy images. It would be strange for it to use anything from the Infrastructure layer directly.

The only layer that needs to call the database, send emails, and other shenanigans is the Application layer. On top of that, the Infrastructure layer needs to refer entities, store them in DB, or send them via event bus. A perfect placement for it appears on its own.

If you look closely at this layer you can see that regardless of the same functionality, it contains unrelated components. It is unlikely for data access to use event bus, or for blob storage to send emails. Those are just pure independent utility classes.

The easiest way to prohibit one class from calling another is by splitting this layer into different modules.

You can go even further and split each module by interfaces and implementations.

It will allow you to easily replace implementation details without changing interfaces and all the components that depend on those. That was a big thing in ports and adapters architecture.

Having multiple small modules is what low coupling and high cohesion are about:

Low Coupling —refers to the degree of dependency between different components in a software system.

It is desirable to avoid dependencies for independent modules with minimal interactions.

High Cohesion — refers to the degree to which the elements within a module or component are related to each other.

Classes that share the same responsibilities should belong to one module.

There are other benefits from using interfaces like dependency inversion and so on, but those are for other stories 🙃. Meanwhile, we are done.

How to be pragmatic?

If you have been following all guidelines from this article you should end up with something like this.

This architecture encourages all best practices and principles. You can even continue decomposing components trying to prevent even more mistakes. However, there are some issues. While further improvements yield minimal benefits, the effort to implement them significantly increases. You have to maintain all the dependencies, and relations between different abstractions, make sure layers do not intersect, and so on😒. In the end, the solution becomes overly complex and difficult to understand or maintain.

So what should we do then?

I would suggest to get back to those four layers:

It is straightforward and simple.

Yes, you can still make mistakes like injecting domain services into domain entities, using domain services in infrastructure utilities, or using an event bus in data access, etc. Even though those mistakes can be made, those are junior-level mistakes that can be easily addressed during PR review.

Layers intend to isolate code by functionality. But at some point, even those just won’t be enough to deal with complexity. It is the inevitable fate of every project to grow in size. Instead of introducing additional layers, there is a better approach.

You can isolate code by business area with vertical slices, bounded context, modules, microservices, and so on. Just make sure you don’t overengineer😉

Wrapping it up

As you can see there is no golden bullet 🤷‍♂️. Each approach has its own advantages and drawbacks. Aim to meet requirements and efficiency without adding unnecessary complexity.

I hope now you have a better understanding of software architecture, layers, and dependency between those. Necessity of each. What they contain. Stuff like low coupling and high cohesion should be demystified for you at this point. But most importantly, you have learned how to isolate codebase in a scalable manner.

Let me know in the comments what layers you have in your project💬
Give this article a clap or a few if you like it 👏
You can support me with a link below ☕️
Don’t forget to follow to receive more architectural insight ✅
And keep your code healthy, avoid spaghetti, don’t let overengineering make it petty, let it flow steadily 😉

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence