There are two main approaches to structuring your app's code: horizontal layers and vertical feature slices.
Not long ago, I had a hard time implementing a new feature in a .NET Core WebApi project which happened to be structured by layer. I was thinking, "Why again a layered codebase?" when I realized that most brownfield projects I've contributed to so far as a developer followed the layered approach. I'd always have a hard time with these kinds of codebases. The reason is, when I work on a feature, it just feels natural to me to co-locate all involved components rather than spreading them to a maximum degree across the whole codebase.
I don't want to explore the reasons leading to layered codebases in the first place here - I believe they are manifold. Instead, I will explain the benefits of feature slices from my perspective as someone engineering line-of-business applications based on .NET backends for most of the time.
Before we dive into the various aspects positively influenced by feature slices, let's shortly recap both approaches and establish a common baseline.
Or, just skip the intro and jump to the sections directly:
- First impression
- Mental overhead
- Single responsibility
Technical aspects and infrastructure are the main drivers for the layered approach, regardless of whether we look at the frontend or backend part of an application. Here, components are strictly organized according to their kind.
For example, when working on a typical .NET Core WebApi, we find namespaces (or even projects) dedicated to controllers (exposing HTTP endpoints), data transfer objects, services implementing business logic, business objects, and data access.
A React app following this structure would have its code organized into folders containing components, hooks, state management, data access (talking to Web APIs), etc.
On the other hand, feature slices reflect the high-level use cases or topics of an app, with top-level namespaces or folders named accordingly, e.g. Login, BrowseProducts, or Checkout.
That does not imply that there is anarchy in terms of component responsibilities within a feature slice. Typically, there too will be components encapsulating business logic, data access etc. The difference is that these components live side by side as they collectively provide a particular feature. In other words, the layers are still intact but not etched into the project's structure.
Of course, if a feature is very complex, breaking the structure down into several sub-features is very common. Likewise, the layered approach: It is often possible to identify the features the app is providing, be it through namespacing or component names within a specific layer.
Both approaches also go by other names. For example, onion architecture and horizontal slices refer to layered structures, and vertical slices refer to feature sliced structuring.
Having the basics out of the way, the remainder of this post will outline why I favour feature slices.
Remember the last time you skimmed over someone else's source code trying to figure out what it tries to achieve?
If you find yourself digging deep in folders or even single files to get a rough idea of the features implemented, you probably look at a structure-by-layer. However, the high level how, infrastructure-wise, becomes evident after only a short reading time. Often it is hard to tell one layered codebase from another, especially when looking at those built on the same tech stack.
With feature slices, on the other hand, the source code naturally reveals the implemented features, as the topmost folders are named accordingly. Infrastructure concerns are hidden within each slice and do not block the view of the essential.
That said, I consider the what more important than the how when skimming over codebases.
We developer constantly navigate codebases: when implementing a new feature, extend existing ones, searching for the right line to set a breakpoint etc. Of course, today's IDEs help a lot in this regard with powerful search features or filters no one wants to miss.
The truth is we don't always know what to search for. Often we end up exploring the codebase by clicking through its folders, which is especially true for developers who are new to a codebase but probably also valid for those who have been around for a longer time and returning to work after an intense weekend or vacation.
Feature slices help to navigate the codebase efficiently. We drill down the folders by feature (and subfeatures), and the main components in charge reveal themselves.
In contrast, a layered structure needs more brainpower as the components within a layer are not always clearly cut-by-feature. We have to remember which logic got where and sometimes, several files need an investigation before we find the right one.
Things get worse the more complex the source code is. But to be fair, complexity affects both approaches. An excellent layered codebase potentially outperforms a poorly structured feature-sliced one in terms of navigation effectiveness.
For me, one of the biggest pain points when working with layered codebases is that I have to jump back and forth between those layers when working on a single topic. The sort of client projects I usually work on has accumulated code over several years. Their sheer amount of folders and files makes navigation a constant annoyance considering their layered structure.
This aspect is closely related to navigation. Navigation requires us to have the way (the sequence of clicks necessary to reach a particular file) in mind. Likewise, when adding or moving components, we need an overall idea of where to put what.
Working with feature slices in this regard is a no-brainer as this structuring approach keeps the mental overhead to a minimum. Once we find the right location to work on (which is easy, as explained in the last section), we rarely need to navigate away from it during the work on a distinct feature. As a result, we see ourselves less exposed to unnecessary distractions and can longer hold the flow.
On the other hand, layered codebases require us to have our navigation system turned on the whole time. We switch back and forth between layers, and choosing the correct places for new components requires conscious decisions. These things turn our focus away from the real problem at hand, disturbing the flow.
Imagine we are in the middle of implementing the backend part of this new feature. We have to write a repository class, and it needs an interface so that we can mock it away when unit testing the consuming service. Questions arise:
Is it ok to put the interface right beside the repository, or was there a different place for all the data access interfaces? (Yes, there are still projects out there who prepare themselves for the unlikely event of swapping out the persistence layer somewhen in the future.)
Same with the data types returned by the repository. They should go where the interface goes, right? Or is there a special place for them too? Oh wait, there is already a similar type; it only needs two additional properties. Do we reuse it here? We could make it a base class (which the author would never do, btw). And so on.
Sure, once we get used to it and visited the layer places several times, placed new code here and modified others there, we can keep this kind of distractions to a minimum. But still, what remains is this nagging thought that things could be much easier.
While this one is debatable, I would argue that vertical feature slices potentially result in less code in need of documentation. At least a developer is not encouraged to write more comments than necessary.
Is that a good thing? I'd say yes because documentation should only explain things that are not obvious as good code usually can speak for itself: the reasoning behind a chosen approach, the constraints considered, the trade-offs been made, and of course, the explanation (or reference to it) of a particular algorithm used.
In my experience, it is pretty common for layered codebases to contain comments which try to compensate for the distribution of code belonging to a specific topic or feature: "This is used by ..."
Feature sliced code does not require such explanations. At first glance, it may seem a minor point, but for me, seeing those bread crumb comments which strive to make up for the lost cohesion reveals a major structural flaw.
Feature slices help to keep components' responsibilities focused. To put it another way: With layering, it is easier to take shortcuts, leading to a violation of the single responsibility principle (SRP).
Why is that? Well, this is more of a subtle matter. A layered codebase tempts us to leave the SOLID path as it is much easier to put code for a new feature into an existing component instead of beginning a new one. The reason is that the components of the same infrastructural type (controller, service, repository) live side-by-side. For an inexperienced developer, it's not much of a difference. For others, it's just convenient if in a hurry or the SOLID defences are down for some reason (fatigue, stress, distractions).
That said, feature sliced code not automatically adheres to the single responsibility principle. It just allows the developer to follow more easily here. An awareness must nevertheless be present. Otherwise, you might fall into the trap of writing components spanning multiple layers, e.g. business logic entangled with data access, which can hurt testability.
One example of how features slices foster single responsibility is the registration code for dependency injection (i.e. mapping of interfaces to concrete implementations, factories; Autofac is a prominent representative in .NET stacks). Where should this registration code ideally go?
Most of the time, I've seen layered codebases (but not only those) with one massive registration class plugging together each and every piece required to get things going, no matter the layer or feature.
Feature slices provide a natural way out of these big-ball-of-mud-registrations. Each slice can have a dedicated registration class nicely encapsulating all the nitty-gritty details of the feature's inner workings (e.g. an Autofac module). A concise top-level registration class only has to pick up the feature registrations — a big plus for maintainability.
Another observation related to the last one (SRP) is that layered code promotes generalization since it is more likely to find similarities between components in the same layer. Often we then feel the urge to treat those with the powerful cure called generalization.
While applying generalization may improve code in some situations, we must be careful and consider passing on this kind of refactorings if the components have nothing in common but parts of their structure. Too often, seemingly simple changes to one feature break another because generalization assumptions for those two made in the past are no longer valid today.
Generalization is a double-edged sword. It may help in some regards, but it shoots us in the foot if not watched closely. And sleep with an extra eye open if in layered mode.
I can imagine most of us came across this component in a business layer whose sole purpose is to delegate to data access code and often only mirrors the corresponding part of the repository API in question, without even any data mapping. Such kind of component is called anaemic. It does not contribute any functionality; it just adds more bloat to the code.
Anaemic components are a code smell, usually found in layered codebases. Often, dogmatic obeyance (in the form of coding guidelines or strong opinions of leading developers) to the dependency chain (e.g. WebApi Controller -> Business Layer -> Data Access Layer) prohibits skipping a layer if circumstances would allow it.
Not seldomly, we see codebases split up into several modules, one for each layer, and the allowed dependencies between them restricted (e.g. by conventions or tools like NDepend) to enforce the dogmatism described above.
As a result, we see a good amount of anaemic stuff lingering around and an otherwise unnecessary amount of modules.
From a technical perspective, it is sufficient to have the codebase partitioned into modules according to the deployment targets and the shared stuff. Since a typical application only has a limited set of deployment targets, such as CLIs or web services, the number of modules required is usually low.
In defence of those extra modules, we hear arguments like: "We can reuse the business logic elsewhere in the future." or "We can swap out our persistence layer if required! How cool is that?!".
In my opinion, these are just weak attempts defending hopeless cases of YAGNI. I consider these extra modules (per layer) as physical namespaces and completely pointless.
In contrast, feature slices stand in the way of these common malpractices due to their intrinsic nature being vertical spikes penetrating all layers.
Each slice can start simple and evolve only to the necessary degree of complexity. Why not letting a WebAPI controller action talk to a repository directly if there is absolutely no logic in between?
Superfluous modules naturally feel out of place in feature sliced codebases. Their uselessness should become evident to the strongest advocate of physical namespacing. Of course, unless someone suggests having a module per slice 😱.
The choice between a layered and a vertically sliced codebase significantly impacts its maintainability and developer productivity.
Even though it is not always immediately apparent what positive effects feature sliced codebases bring to the table, we should not dismiss this structuring approach lightly.
Compared to layering, I find feature sliced codebases:
- are more accessible - what is where? - emphasize the what over the how
- contain fewer bloat - start simple, complexity evolves as required
- are better to maintain
- improve developer productivity
- foster pragmatism
- are more fun to work with 😀
That doesn't mean that feature slices are the silver bullet to all our problems. Reasonable design decisions, feature partitioning, and lots of discipline are nevertheless in great demand to make our projects succeed.
Thanks for reading this! I'm keen to hear about your experiences and opinions regarding codebase structuring.
Take care & happy coding!
For reasons of simplicity, I use the term component synonymously for all sorts and levels of encapsulated logic: classes, functions and compounds of those.
The term module refers to a compile target here. In .NET, for example, those are called assemblies.