Onion, Hexagonal, Clean, or Fractal Architectures aim to organize how we deal with dependencies in our software architectures. But which one should we choose? After distilling the essence of each approach and comparing the advantages and challenges, I’ll show how to combine all of them into an approach to use evolutionary steps towards an architecture that fits your needs from day one until the software dies. You’ll see that layers and slices aren’t enough. A modularisation that fits the domain and simplifies understandability, changeability, and extensibility must go beyond these concepts.
These are the slides with notes from my presentation about the architecture patterns mentioned in the title and how we apply them in our application.
When mankind started writing programs – that’s what they were called back then, not applications – they were small. Small means they were “easy” to understand and maintain because little code existed. But soon, these programs grew bigger.
When the codebase grows bigger, there is a need for organisation. Otherwise, we can’t navigate the code quickly anymore, the code gets harder and harder to understand and maintain.
From this need, the idea was born to separate code elements by concerns. For example, the code could be put into buckets containing all the things related to the user interface, business logic, or persistence. The reasoning is that, for example, all the persistence codes look alike, so we put all the code that looks the same into the same place.
Before we continue, here is a short disclaimer: A technical term always has three meanings. First, the meaning as the inventor first described it. When the idea gets some traction, the meaning often changes slightly—or sometimes even significantly—because the inventor adapts the meaning to the feedback received or to newly encountered situations. That’s the second meaning of a term – what the inventor made the term to mean over time. Lastly, when an idea becomes mainstream, the community typically misunderstands the initial ideas, and a new meaning arises.
An example is BDD—behavioural-driven development—by Dan North. Initially, he just replaced the noun Test in Test-Driven Development because developers better accepted the ideas from TDD when it’s not about tests but behaviours. So, it started as BDD being the same as TDD. Over time, Dan North saw that the ideas could be applied to a broader scope and moved the meaning of BDD more towards ATDD—acceptance-driven development. When the community and tool companies joined in, BDD became a lot about tooling, like Gherkin and SpecFlow.
Therefore, when you disagree with me on how I use a term, please take a moment to reflect on whether it’s a disagreement because the term has several meanings. In this presentation, I try to stick with the initial ideas of the terms.
Now, let’s go back to our three layers. I don’t know what drove the introduction of horizontal layers in software, but I have two guesses. Either it’s an extension of the ISO-OSI model for computer communication (with its seven layers from the wire via session to the application layer) into software, or it just matched how we split work amongst a team into database, business logic, and user interface developers.
There are two versions of horizontal layering: strict and non-strict.
In strict layering, a layer can only call things from the layer directly below. This reduces the number of callable APIs and thus comlicatedness, but it introduces a lot of pass-through and mapping code from layer to layer.
In non-strict layering, a layer can call anything below. This reduces boilerplate code but makes it harder to find the right thing to call and to see what source-code depends on each other.
Make your trade-off.
Layered architectures have been mainstream for business applications for many years. And they are probably still used widely.
But they have a problem. When UI code is allowed to access persistence code, persistence code is very likely to bubble up the layers. I started with .Net 1.0 and remember the days of ASP.NET web pages code-behind files cluttered with SQL queries.
An idea came along that we should flip the dependency direction so that business logic does not depend on the persistence layer. So that the persistence code could not bubble up anymore.
Alistair Cockburn came up with the ide of the hexagonal or ports and adapters architecture.
A side note: if you ever invent something, please give it a single name. It makes talking about it much easier 😊
In hexagonal architecture, the application—the business logic and model—is at the centre. When a user (user interface) or service wants to interact with the application, it has to go through an adapter that calls a port on the application. When the application wants to interact with a database, the file system or another service, it must go through an adapter.
As a result, the application knows nothing – at least no technical details – about its environment. Only the adapters know the outside and the inside and connect them.
A consequence of this approach is that the application gets easy to test by replacing the production adapters with fake adapters that simulate the environment.
However, we still have a problem. We can still have a mess inside the hexagon when the application grows. Hexagonal architecture does not give us any advice on how to structure the inside.
In 2008, Jeffrey Palermo wrote a blog post series explaining how they tackled this problem. They split the application not in horizontal layers but in concentric layers, like the skin of an onion. In the centre is the Domain Model, the business domain modelled by types and methods. Around the domain model, there are first the Domain Services, representing the business workflows, and then the Application Services, providing the application’s functionalities. These layers build the application core – the same as the application in hexagonal architecture.
The outermost layer takes the role of the adapters from hexagonal to implement the code needed for the user interface and talk to the database, file system or other services.
Jeffrey Palermo also introduced the fundamental rule that all code can depend on layers that are more central, but code cannot depend on layers that are further out from the code.
The dependency inversion principle is applied when a domain service gets data from the database. The domain service defines the interface and how it wants to interact with the database, and the interface is implemented in the infrastructure layer. The instance implementing the interface is injected into the domain service.
You have an Onion Architecture when you fulfil the four tenets shown in the picture above.
Maybe you think now, I now this stuff, but under a different name. If so, then you probably mean…
… the Clean Architecture by Robert Martin.
In 2012, Robert Martin wrote a blog post introducing the Clean Archtiecture. He states that there are several ideas around in the architectural space that all almost look the same. He condensed these different ideas into what he called Clean Architecture.
As in Onion Architecture, there are concentric layers. He uses different terminology to name the layers—more general terms—and there is no fixed number of layers. Yes, there is no fixed number of layers! However, the dependency rule applies that code can only use code in the same layer or a layer closer to the centre.
Robert Martin added an example of how the flow of control can jump inwards and outwards. The dependency rule is for static dependencies between types, not for the control flow. When a call comes into the controller, the controller can call a use case (closer to the centre); the use case uses dependency inversion through an interface or a function to get the entities from the database, executes code on the entities, and returnes the data back to the user interface through the presenter, for example.
Additionally, Robert Martin clarified which data should be passed over the layer boundaries. He stated that only simple data structures, like DTOs or records, should be passed. These structures should be in the most convenient form for the inner layer. Instead of focusing on the data in the database and making it easy to interact with it, the structures should make it easy to interact with the entities and use cases.
Still, the Clean Architecture approach has its problems.
The initial blog post states that changes in the use case layer should not lead to changes in the entities. Maybe this was correct back in 2012, when we did more design up front and started by modelling the domain and then derived the use cases. But nowadays, when we build software feature by feature—at least I hope that is how you develop your software—my experience is very different. Almost every change in the use case layer affects the entities because the entities grow together with the use cases. So, every change goes through all the layers.
And how should we organise the things inside a layer?
It was time for a new idea: grouping together by functionality, not by kind of job. Instead of having horizontal layers, we focus on vertical slices. A slice per feature.
That’s what Jimmy Bogard calls the Vertical Slices Architecture. In his initial blog post, he gives some examples of slices and how they could be implemented (see picture above).
The idea is that there is no single architecture for the whole application. We can decide per slice how it should be designed and implemented. As simply as possible.
Jimmy Bogard observed that when designing with slices, most abstractions melt away. There is no need for all the interfaces you have to introduce in a Clean Architecture to enable dependency inversion—unless the slice is complicated enough for these interfaces to provide value.
We can also implement feature by feature, just like we implement user story after user story (or task after task, or feature after feature, or however you want to call it). It’s a good match with our overall approach to implementing software.
But again, there are still problems.
If you have a more extensive system, you’ll have a lot of slices. A big bucket full of slices isn’t understandable and maintainable.
And there is the question of how to deal with thangs (classes, types, functions etc.) that are shared between slices. If you have a “create customer” slice and an “edit customer” slice, they likely share the model of the customer and some validation logic. Where should we put this shared code?
In his presentation, Jimmi Bogard dodges these questions by more or less saying that a team skilled in refactoring will find a way. This is true, but it leaves us without ideas on where to continue.
Therefore, for the rest of the presentation, I’ll give you the ideas we use to deal with this shared code and the sheer number of slices. It’s not meant to be copied blindly but as inspiration.
First, we have to take s short detour:
Mark Seeman came up with the idea of the Fractal Architecture. Based on the fact (actually, I’m not so sure that it’s a fact, but let’s run along with it) that we need to read around ten lines of code per line of code we write, we should optimise our codebase for readability.
A critical aspect of readability and understanding is that our working memory can only keep track of up to seven chunks. Simply put, a chunk is a single piece of information we can load into our working memory. What a chunk is, is different from person to person because their experience and knowledge play a significant role.
If you are A C#, Java or C++ developer, you are very used to if-statements. Although an if-statement consists of the keyword if, a condition, a couple of braces, and a body, for your brain, an if-statement is a single chunk. You can read and understand an if-statement as a single unit of information. I’m also an F# developer, so a fold (same as Aggregate in LINQ) is a single chunk for me (let x = [1;2] |> List.fold (fun state element -> state * element) 1
). If you are not used to folds, the fold itself is probably already seven chunks in itself! That’s one of the reasons we have difficulties learning programming languages that have different chunks from the ones we are used to. It needs time to build up the chunks.
If we apply the idea that we should only be confronted with at most seven chunks at any place in the application, we get something self-similar, a fractal. In the picture above, the black box represents our whole system. We decompose it into at most seven subsystems, each subsystem into at most seven elements, and so on. We get a kind of fractal structure.
Mark Seemann gives a couple of examples in his talk about Fractal Architecture.
Here, he shows some code on a very low level dealing with persistence. He states that for him, this code has six chunks: the arguments to the method, the null check, building the connection, and its arguments.
On a higher level, in the code configuring the controllers, he finds again six chunks: the argument, the signing key, the configuration of the controller, and so on.
Finally, on the top level, where the program is configured, he sees a single chunk, the StartUp
class.
Please remember that chunks are individual and that his chunks are probably different from your chunks or mine.
Now, let’s take a look at our application, and how we apply all the things we’ve seen so far:
Our application is called Time Rocket and is an attendance time tracking application with duty planning, expenses and project time tracking.
We split our application into several sub-systems (simplified):
- Employment: who works for which company under which contract
- Attendance: when does someone start working and stop working
- Absence: when was someone sick or goes on vacation
- Duty Planning: when are the duties and who takes them
- Undertakings (a fancy, more general term than projects): on what projects and tasks do we work, and how long
- Expenses: yeah, we want our money back😊
- Accounting: the place where all the numbers are crunched to know how much I still have to work and how many days I can still go on vacation
- Cost Centers: I work, for example, for the cost center Development
- Automation: one of our unique selling points is that we can automate many tasks, the automation infrastructure can be found here
- Customisation: customise the application to the needs of the customers and users
These are the top-most slices, or sub-systems, as we call them.
Every slice is then decomposed into sub-slices. The Undertaking slice consists of the following sub-slices: Structures define what you track. For example, customers, order, and project; or maybe facility and task. Undertakings can be grouped. We have permissions. There is the actual data on the undertakings. Multi-actions are used when you work on multiple things at once, for example, cutting pieces for several orders in a single run -. Activities hold the data when somebody works on a task; for example, I’m writing this blog post from 13:00 to 17:00 on 19.09.2004 for the project called Marketing. There are budgets and settings to configure all the above.
When we zoom into the activities slice, it again consists of sub-slices. There a manual activities (I enter them manually), multi action activities (when I work with multi-actions) and activities generated from attendance time (I state my default project and all my attendance time goes onto that project automatically).
Finally, when we look into the manual activities slice, we see that we can create an activity, replace it, or delete it.
We failed regarding the “at most seven elements” guideline, but it’s manageable.
You see, we have kind of a fractal of slices: slices within slices within slices.
Now, I can explain where we put code that is shared between several slices.
When, for example, the shared code of the create activity and replace activity slices (the model of an activity and the validation logic) finds its place in the slice one level above, the manual activity slice.
Shared code between manual activities and multi action activities is put into the slice activities, one level up.
We can also take a look at our application regarding layers.
Let’s stick with the example of creating a manual activity. The call from the web client is handled by a Asp.Net controller, the Activities Controller. It is defined in the infrastructure layer, according to Onion or Clean Architecture.
The controller does some JSON deserialisation and calls a facade: the Activities Features facade. We use a facade because the controllers, Azure functions, or test code call the code behind the facade. That’s kind of the adapter from Hexagonal Architecture.
Then, the create manual activity feature (or command) is called. That’s the use case from Clean Architecture.
The create manual activity feature uses the model of the activity to perform its job.
Finally, we call a function that persists the data in the database. The function is implemented in the infrastructure layer and injected into the create manual activity feature.
When we take combined look – layers and slices – at our application, we see that we have sliced each layer into slices. The slices in each layer do not correspond to each other one to one. Often, a slice in an outer layer uses several slices of an inner layer. The controller not only calls the facade to execute the command but also authenticates it using the security slice. The feature does not only use the corresponding models but also writes an audit log using the security slice.
Let’s take a look at our solution explorer. Here, you can see that we introduced a solution folder per top-level slice/sub-system. There is a bit more: Composition contains the code to build up our application, Core is the oldest part of our system that uses slicing inside a single project, and Fundamentals is the shared code on the top level.
Under Undertaking, there are three projects: the code for the Web API, the business logic, and the tests. We split that because we can’t reference the Web API packages in the core code; otherwise, it would be a mess to reference them from our Azure Functions.
When we zoom in on the Undertakings sub-system, you can see that the API layer is sliced into controllers matching the slices shown earlier. The business logic code is first split into layers: models, features/use-cases, and facade). Because this is F# code and F# has strict ordering of types – you can only reference what is declared before; the layers are visible in the solution explorer. This is a bit strange when starting with F#, but it is a great feature to tame a codebase.
So, the facade is at the bottom, above are the features/use-cases, and on top, the models (entities in Clean Architecture).
If we zoom in again, we see that the activities slice in the models layer is split into manual activities, multi action activities, and activities from attendance.
There is also the persistency implementation (ActivitiesStorages) because having this code close to the models is nice. When the models change, the persistence often has to change as well.
Let’s zoom in a final time. Inside the manual activities slice in the models layer, there are the models for the manual activity and the model for the event—we have an event-sourced system.
On the features layer, we find the commands to create, replace, and delete a manual activity. Also, the shared code for validation is put into the manual activities slice
Authorisation is shared among all features and lies directly in the features layer.
The queries to get all the different activities are also in a higher-up slice because we want to read all kinds of activities at once from the database for performance reasons.
As you can see, we have a mix of layers and slices. Once you get used to this kind of code organization, navigation and changing code become relatively easy. It’s also imperfect, but we are pleased with this approach.
It is time to reflect on what we saw in this presentation. All the things we talked about so far concerned the backend, but there is also a user interface, a database (and probably some other services). So, are we back at the three horizontal layers again?
When I did my research for this presentation, I had the strong impression that all these architectural patterns were driven by backend developers.
However, we can apply the same concept, at least partly, to the user interface and the database. We can also apply slicing to the UI. However, we often have to slice differently than in the backend because we need to show data from different backend slices in a composite UI. To organise the database, we can use schemas. We use a schema per sub-system; it’s good enough for us.
Let’s sum up. I have shown you how we take the ideas of all the different architectural patterns and build a consistent whole in our application. We still have horizontal layers, we use the dependency direction rule from Onion/Clean Architecture, we use the recursive nesting from Fractal Architecture, we use adapters like in the Hexagon Adapters – we use them in inter-sub-system communication – and we use vertical slices so we have the code for a single feature close together.
Please don’t copy our approach. The message you should take from this (way too long) blog post is that you should look at all the patterns out there and combine them in a way that works for you.
I hope I gave you some inspiration.