Note: I created a talk based on this article that you can view here.
“Clean Architecture” is a software architectural pattern coined by Uncle Bob Martin in his book called, naturally, Clean Architecture 😅. It’s one way to structure software code that is an example of hexagonal architecture.
Ports And Adapters
The basic idea of a hexagonal architecture, otherwise known as a “ports and adapters” architecture, is that your domain logic and domain objects live in the “center” of your application. Other concerns like persistence, caching, etc. are treated as an add-on or “adapter” to the domain code.
Generally, this is done by having your domain code expose any need for these infrastructural or application-level code as an interface.
This is conceptually similar to how we might design a computer, for example. A computer has a dedicated core purpose, but allows outside devices to “plug-in” via some kind of interface (a USB port, HDMI port, etc.)
Principles Of Clean Architecture
Clean architecture has a number of principles that I’ll summarize here:
- Independent of Frameworks
- Independent of UI
- Independent of Database
- Independent of any external agency
At first glance, I think most software developers would agree with these principles. I do, for the most part 😉 (we’ll get that that later…).
The most important principle that comes from both clean architecture and domain-driven design is to keep your domain objects and domain logic separate from other concerns.
We need to decouple the domain objects from other functions in the system, so we can avoid confusing the domain concepts with other concepts related only to software technology or losing sight of the domain altogether in the mass of the system.Domain-Driven Design pg. 67 (Kindle)
How We Tend To Move From Conceptual Patterns To Implementation
The thing I don’t like about clean architecture is not necessarily on principle, but around how most developers move from thinking about these principles to implementing them into actual software projects.
The typical structure is to create projects or libraries that deal with:
- Application layer
- Domain layer
- Infrastructure layer
- UI layer
We tend to take these concepts and “one-by-one” just implement them as dedicated .NET projects, Java packages, etc.
I believe that this way of moving from the conceptual thinking to applying that as a specific “blueprint” of how you ought to structure your code is faulty.
Problem’s I’ve Faced With Clean Architecture
In one of the companies I’ve worked for, I brought in clean architecture as one of many ways to start improving development processes, development speed and product quality.
We implemented clean architecture for new features moving forward. While this was way better than the non-existent conventions and architecture that existed previously, there were aspects that caused issues.
One of the biggest ones was content switching.
For example, if you were working on a feature and needed to work on any combination of data access, domain or application logic, you had to totally switch contexts into a different software project. Into a totally different folder structure.
This seemed inefficient.
I decided that putting everything relating to a specific feature together, in the same folder, brought more benefits than not.
While respecting the dependency rule, this idea is the logical conclusion of following the principle of high cohesion: things that change together ought to live together.
But, doesn’t that lead to a muddling of concerns? Application, domain and database logic in the same place?
If you understand the principles of high cohesion and loose coupling, then this shouldn’t be a problem.
You can still acheive the “ports and adapter” like approach conceptually, but within each individual feature.
For example, the typical approach I take is something like this:
If we “zoom-out” and are looking at this from the view of having multiple features or products, it looks like this:
Notice that I’ve grouped the “application” and “domain” layers under the same bucket. That’s because, in many cases, these can simply exist within the same folder or software artifact.
Within the same feature folder, if you keep your data access and other IO behind an interface, then “Bob’s your uncle.” 😋
At the time, I had come up with this approach on my own by using the principles of loose coupling and high cohesion.
As I stumbled through this way of thinking, I came across what is commonly known as “vertical slices” architecture.
Jimmy Bogard, as far as I can tell, is responsible for codifying this approach.
In my experience, this approach is simpler.
When adding or changing a feature in an application, I’m typically touching many different “layers” in an application. I’m changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice. Minimize coupling between slices, and maximize coupling in a slice.https://jimmybogard.com/vertical-slice-architecture/
It focuses not on the separation of concerns from a technical perspective (like domain vs. infrastructure vs. application logic) but from the perspective of reflecting the business domain’s features.
Again, keeping data access separate from domain logic is key, no doubt.
But, should that drive us to separate these things as separate software artifacts?
I think it makes more sense to separate business capabilities as separate software artifacts instead.
And now, we end-up in the land of SOA. But that’s another topic for another day 😅.
Do We Ever Swap Out Databases?
Proponents of the architectural style I outlined as the typical “clean architecture” might raise some objections to the vertical slices approach.
The primary one being: What if you need swap out your database from MySQL to Oracle?
My response would be two-fold:
- How often does that actually happen? This sounds like premature optimization. I believe we can gain immensely more from keeping highly cohesive pieces together.
- A vertical slices approach can actually make this easier!
About that last point: since we focus on making separate features in our system loosely coupled and isolated from each other, it follows that we can totally “re-do” an individual feature and keep that change isolated.
All too often, these migrations end up being a Frankenstein data access layer because it’s “half-way” into it’s migration.
If this interests you, here are a couple resources that can help you get started: