Software is an instrument created to help us deal with the complexities of our modern life. Software is just the means to an end, and usually that end is something very practical and real. Software development is most often applied to automating processes that exist in the real world, or providing solutions to real business problems; The business processes being automated or real world problems that the software is the domain of the software. We must understand from the beginning that software is originated from and deeply related to this domain.
Chances are that you have developed a layered (web) application in the past. You might even be doing it in your current project right now (actually, I am).
Figure 1 shows a high-level view of the very common three-layer architecture. We have a web layer that receives requests and routes them to a service in the domain or “business” layer. The service does some business magic and calls components from the persistence layer to query for or modify the current state of our domain entities.
So, what’s wrong with layers? In my experience a layered architecture has too many open flanks that allow bad habits to creep in and make the software increasingly harder to change over time. In the following sections, I’ll tell you why.
It Promotes Database-Driven Design
By its very definition, the foundation of a conventional layered architecture is the database. The web layer depends on the domain layer which in turn depends on the persistence layer and thus the database.
Usually, we have ORM-managed entities as part of the persistence layer as shown in figure 2. Since layers may access the layers below them, the domain layer is allowed to access those entities. And if it’s allowed to use them, they will be used.
This creates a strong coupling between the persistence layer and the domain layer. Our services use the persistence model as their business model and not only have to deal with the domain logic, but also with eager vs. lazy loading, database transactions, flushing caches and similar housekeeping tasks.
The persistence code is virtually fused into the domain code and thus it’s hard to change one without the other. That’s the opposite of being flexible and keeping options open, which should be the goal of our architecture.
It’s Prone to Shortcuts
In a conventional layered architecture, the only global rule is that from a certain layer, we can only access components in the same layer or a layer below. There may be other rules that a development team has agreed upon and some of them might even be enforced by tooling, but the layered architecture style itself does not impose those rules on us. So, if we need access to a certain component in a layer above ours, we can just push the component down a layer and we’re allowed to access it. Problem solved.
Doing this once may be OK. But doing it once opens the door for doing it a second time. And if someone else was allowed to do it, so am I, right? I’m not saying that as developers, we take such shortcuts lightly. But if there is an option to do something, someone will do it, especially in combination with a looming deadline. And if something has been done before, the threshold for someone to do it again will lower drastically. This is a psychological effect called the “Broken Windows Theory”.
It Grows Hard to Test
A common evolution within a layered architecture is that layers are being skipped. We access the persistence layer directly from the web layer, since we’re only manipulating a single field of an entity and for that we need not bother the domain layer, right?
As developers, we like to create new code that implements shiny new use cases. But we usually spend such more time changing existing code than we do creating new code. This is not only true for those readed legacy projects in which we’re working on a decades-old codebase but also for a hot new reenfield project after the initial use cases have been implemented.
Since we’re so often searching for the right place to add or change functionality, our architecture should help us to quickly navigate the codebase. How is a layered architecture holding up in this regard?
As already discussed above, in a layered architecture it easily happens that domain logic is scattered throughout the layers. It may exist in the web layer if we’re skipping the domain logic for an “easy” use case. And it may exist in the persistence layer if we have pushed a certain component down so it can be accessed from both the domain and the persistence layer. This already makes finding the right spot to add new functionality hard.
But there’s more. A layered architecture does not impose rules on the “width” of domain services. Over time, this often leads to very broad services that serve multiple use cases.
No comments:
Post a Comment