Petar Marinov

Layered Architecture and Features

25th May 2020
Software Architecture
Layered Architecture
Modular Design
Vertical Slices
Last updated:19th January 2025
6 Minutes
1064 Words

Background

Layered Architecture is very popular and many software systems are built using some variation of Layers and the Dependency Inversion Principle. (Onion, Hexagonal, Ports and Adapters, Clean, etc.)

The business will define a unit of work bringing value to customers as a feature. (This is not to be taken as universal truth, rather it is my experience with business systems.) To implement a feature, one needs to touch multiple layers and deal with multiple abstractions.

So there is a tension between layers and features - layers abstract and group by technical aspects (persistence, presentation, business logic, etc.), whether features tend to span over multiple layers to bring value.

Let’s assume layers are used as a system-wide architecture. This is a simplified presentation of how layers and features relate:

default

To implement the first features, the layers need to be defined upfront. Once layers are in place, every feature should comply with them. A decision having such a big impact on future development is forced too early.

Layers enforce abstraction - a good layer design is to have an internal and external representation of the entities it handles. There are cases when this does not bring any value, e.g. simple CRUD or reporting. This is why some define layers as “closed” (it is mandatory to always go through it) and “open” (can be “skipped” in some cases). I don’t find open layers a good practice. Once allowed such “shortcuts” will repeat over time and end up in “opening” all layers, thus defeating the purpose. But if all layers are closed, there will be layers that just proxy to the next layer without doing anything but adding levels of indirection without any value.

Implementing a feature requires changes in multiple layers. In the early stages of development or when prototyping some new feature, this can be a burden.

Future maintenance and development can become complicated - mostly because it is hard to track all the pieces across layers needed for a feature to work. The “big picture” of how a particular feature works becomes blurry and needs significant cognitive effort to understand.

Features tend to have specific requirements. Although layers promote reusability, in most cases each layer will have abstractions serving specific features. This leads to feature-specific “islands” in layers. On the other hand, trying to enforce DRY leads to over-complicated abstractions - a single abstraction serves multiple features, trying to generalize each one’s specifics.

I have to say that I’m not against Layered Architecture, merely that it’s not a good fit for certain cases. When the unit of work is a feature, Layered Architecture may stand in the way instead of bringing value.

When a system requirement is to be able to swap entire layer, if the layers bring significant value by abstracting away complexities, or if teams specialization is focused on a specific area - all these are good fits for Layered Architecture.

Vertical Slices Architecture

One alternative to Layered Architecture is Vertical Slices Architecture. The general idea is to move boundaries from “horizontal” layers to “vertical” slices. Group together what is likely to change together - maximizing cohesion within a slice and minimizing coupling between slices.

default

Vertical slices do not exclude layers. The difference is that decisions can be made per slice. Some features may benefit from a rich domain model and more strict layering and abstractions. For others, a simple Transaction Script may be suitable.

To minimize coupling between slices, some degree of code duplication is tolerated. Ultimately, the cost of duplicated code is less than the cost of wrong abstraction. As the system evolves, duplications are refactored away in the domain model or domain services, and common infrastructure services may surface. One benefit is that abstractions “emerge” from implementation, and decisions about them are delayed to a moment when more information is available.

Modular Architecture

My preference leans toward a Modular design, where each module puts a boundary around business capability. As with Vertical Slices, the goal is to maximize cohesion within a module and minimize coupling between modules. The difference is that multiple slices are combined within a single module, since a business capability may be comprised of multiple features.

default

My own guidelines for building modules are:

  • Modules hide internal implementations. All that’s visible to the world is its public interface.
  • Module boundary extends to its data too. It may be a different storage or a schema within a storage, but data is owned by one and only one module.
  • Modules can communicate synchronously or asynchronously. Synchronous communication (and dependency) is only through the module’s public interface. Asynchronous communication is through messages, be it in-memory or via external message broker.
  • UI is not part of the module. This decoupling allows different module hosts to shape data for external consumption, e.g. web API, console application, or mobile application back-end. Another benefit is that consumers are able to combine data from different modules depending on their needs.

Again as with Vertical Slices, there will be duplication and refactoring cycles. Additionally, the need for boundary adjustments may arise as domain knowledge grows and more features are added.

  • Low cohesion within a module - probably it should be split into separate modules.

  • Two modules can depend on each other with circular dependency. Probably this comes from a wrongly defined module boundary and they should be merged into one.

  • A module has multiple inbound and outbound dependencies. It is worth revisiting the design and reviewing module boundaries.

I see Modular Architecture as something to start with, not the final thing. It requires attention to the code while it evolves. It means continuous refactoring and restructuring while getting more knowledge about the problem being solved. On the other hand, module encapsulation allows the freedom to make use of different approaches - layered, simple CRUD, rich domain, Event Sourcing, CQRS, etc.

As requirements change over time there are multiple directions to go. It may be appropriate to start extracting modules into Microservices. The module’s public interface can become a web API. Messages modules produce or consume can be exchanged via external message broker.

If it makes sense, there is nothing wrong in continuing with a monolithic application in a Modular Monolith fashion.

Conclusion

Layered Architecture solves a certain class of problems. But not every project should use it from the beginning. There is no single architecture that will fit every use case. A project may evolve to layers but should be flexible enough to be able to accommodate different approaches. Vertical Slices and Modular Monolith are two of the possible ways to start.

Article title:Layered Architecture and Features
Article author:Petar Marinov
Release time:25th May 2020