Don't overcomplicate the onion architecture
This is an episode of Thoughts on Functional Programming, a podcast by Eric Normand.
When using the onion architecture, you need to consider the dependencies (actions depend on calculations), but also you need to consider the semantic dependencies (the domain should not know about the database).
Be careful about overcomplicating the Onion Architecture. Hi, my name is Eric Normand. This is my podcast. Welcome. I want to thank you for being on an exploratory journey with me about all these ideas. I don't know [laughs] that these things, I'm just trying to figure them out. I'm exploring them and talking through them on this podcast.
I just want to make that clear. Before we get into the topic today, I want to talk about "Grokking Simplicity." It's my book. It's in print. Now, you can get your copy mailed directly to you.
If you go to manning.com, you can buy it there. It's already available. They're already shipping them. I get notifications from people emailing me or tweeting at me that they've received their copy, but you can also get it on amazon.com. I'm not sure if it will be available.
Probably will be available by the time you hear this, just not sure about the scheduling and when this comes out. It probably should be already available on Amazon, so order it there if that's where you like to get your books.
The book Grokking Simplicity, I should tell you a little bit about it. It is all about introducing someone to functional programming. It's all about how to make pure functions, how to extract out pure functions from existing code.
The second part is all about once you have pure functions, you can start thinking in terms of higher-order functions, abstracting common syntax and boilerplate from your code using higher-order functions.
There's in the second part, the second half of the second part is all about managing actions in the order and repetition of those actions so that you can deal with timing bugs and make sure your software is operating correctly.
Please, do check it out. I've given it a very high-level overview in this. Every page is hand-laid out by me so that it is optimally visually using every page so that you can get all sorts of cool diagrams in there and really understand stuff visually. I'll go over some of these things in a future episode, but right now go check it out. You can see previews of the book on manning.com. Thank you.
Let's talk about our topic today, which is the Onion Architecture and overcomplicating it. In Grokking Simplicity, the last chapter is all about some common architectures, and one of them is called the Onion Architecture.
Very briefly, the Onion Architecture is a layered architecture, but it's an onion. The layers are circular instead of stacked vertically. The inner layer is your pure domain logic, and the outer layer is pure, meaning pure functions, immutable data, all that.
The outer layer is called the Interaction Layer, and this is where all of your actions go. It's where all your impure stuff goes, your reads from the database, writing to the database, hitting an external API, all that stuff goes in your interaction layer.
The layers only depend on stuff inside of them. The outer layer depends on the inner layer, and the inner layer is unaware of the outer layer around it. That's a very brief summary of the Onion Architecture.
In a lot of ways that naturally happens that way when you're writing functionally, and you're properly layering stuff mostly because the actions spread. If I have a function that calls an action, that function is an action. Any function that calls that one is also an action. If you graph that out, the functions at the bottom that don't call actions, those are your calculations.
Those don't know about the stuff above them, and at some point above them is all actions. At some point above, there's no more calculations because the actions spread up. You get this natural layering of your pure stuff and your more imperative, procedural stuff up at the top. One thing that I see people doing is they complexify it. They complicate it. It's hard for me to explain.
I get questions like, "Well, how do I have a calculation that needs to make a read from the database? What if it has to make some decision based on what's in the database?" Calculations are often decisions. It'll be something like, it needs some data.
Then, we'll pass that data in as an argument to the calculation. Then the calculation might determine, "Hey, I need some more data. Can you fetch that for me?" Who fetches that? They can't fetch it because it's a calculation. Then the action above it, it has to exit and return that. It looks complicated.
If you do naturally stratify your code into actions at the top, calculations at the bottom, that's all the onion architecture is. It's making this explicit like, "How could this possibly work?" I'll give a simple example of a web request, and how this fits into the onion architecture.
In a web request, the web request arrives. That is an action. It arrives in your application, your interaction layer. Often, in a web request, you need to make a database read. Let's say it's some GET request for an entity that's stored in the database.
The interaction layer receives this web request. It validates it, which could be a pure function, but it's not domain logic that is valid. Maybe it is. Maybe it isn't. It depends on what you do. It depends on whether you are validating it in terms of domain concepts.
You might validate it for stuff like, "Is this a valid UTF string?" That's not part of your domain. It's computer concepts. If you're writing accounting software, accounting does not have an idea of UTF, eight strings. Accounting software deals with transactions and balances and amounts of money and stuff like that.
You could validate it like, "Oh, a transaction has to have both a from account and a to accounts, a debit account, and a credit account." That is a domain concept. That's, that's domain validation. You have to do both of these kinds of validation.
Anyway, the validation happens, probably a pure calculation. Then, it's going to fetch from the database. This is an action. It belongs in the interaction layer. Once that entity comes back, you have a piece of data. You might have to do some domain logic to it.
You pass it as an argument to some function in your domain layer, your pure layer, your core, and it returns some answer. You take that answer, and you send it back as the response that's also an action. If you listed the steps that the interaction layer took, that becomes like a script.
This HTTP endpoint, does these things. It receives the request. It validates it. It retrieves something from the database. It does some domain operation on it. It returns the result of that operation as an HTTP response. It's a very simple script.
That domain operation though, that's where the important stuff happened. That's all in your domain core, this pure calculation core. Now what I see, like I said before I get questions like, "Well, what if that domain operation decides it needs more data? If it decides it needs to fetch something from a database."
If it does that, then it's not a pure function. It's probably not a domain operation. Let me explain. Yes, you might have a way of returning a value that represents fetch me more data. You might have that.
You have those arrows pointing the right way. You have this interaction layer pointing into the domain. The domain, the functional core, it doesn't know about the interaction layer, it only returns stuff. There's some contract that says, "This is how you interpret this data. It's like, fetch me more data.
The interaction layer will do that, will obey. You do have some sense of the arrows are pointing in the right direction. The problem is semantically you've violated the layers. That domain layer shouldn't know about databases. It shouldn't know about, there's other data that it might need. It needs all the data it needs. You need to pass it all the data it needs.
You might need to do that logic of deciding whether it needs more data. Either do it in the interaction layer, or don't. Pass both. Pass both things in there. Whether you know it needs it or not.
The way you check, is if it's a domain concept. Is fetching more data a domain concept? In accounting, no. There is no domain concept of give me more data. That's not what accounting is about. Accounting, like I said before, has a list of concepts like account, debit, credit, an amount of money, a transaction, these kinds of things.
If you need more data like that's not the right level of abstraction. It's not the right layer in which to code that up. I want to take this idea and push it a little bit further.
If you're making a system, like the domain core makes decisions like, "I need to go fetch more data, or I need to make this API call or stuff like that," if it's making those decisions, then you're making an interpreter in the interaction layer that knows how to deal with all these actions that the domain layer needs.
In essence, you're making an effect system. You're pushing out all this responsibility of carrying out the actions into the interaction layer and deciding what actions to take is part of the domain layer. Like I said, on my previous episode, what's happening there is you're not doing functional programming.
You're doing procedural programming, but deferring by one step when the actions happen. You're missing out on a bunch of the benefit. Besides that, you're also mixing these semantic layers. The functional core of your system should not know about databases. It is not a domain idea, shouldn't know about API calls. That's not a domain idea.
These are not domain decisions. Likewise, they're almost not even business decisions. They're incidental to the way you have architected your system. The interaction layer has all that built-in. By being on the outside, it is the thing that is easiest to change.
Accounting does not change. It is at least in its current modern form, hundreds of years old. There's evidence that...You could find some evidence like, "Oh, this kingdom did it in 2,000, 3,000 years ago." It was kind of it, but it wasn't as encoded as we have it today.
In its modern form, accounting is hundreds of years old. It won't change. Not by the same name. We call it double-entry bookkeeping, that system is not going to change.
You might come up with a new accounting system, that's better. That will be a different system. This double-entry bookkeeping system, you can encode it once. It is timeless, and it will never change.
Your software, as you're developing it's changing. It's approaching an ideal representation of that domain. What changes are your business rules? Those kinds of things. Those can also be pure functions.
On the outside the interaction layer, what database you're using, what API's you're accessing, what file formats you're using, all this stuff, push that to the edges. That changes quickly compared to accounting. I wish I had said this in the book. There's always more to say about this. This is really essential. It's not about the arrows of what knows about what, it's also the semantic. What knows about what.
It's not like the call graph. It's also knowledge. It's also domain concepts. The accounting shouldn't know about HTTP. It shouldn't know about databases. It shouldn't know about API calls where what format you're storing your accounting data in. All that stuff is not important to the domain.
You could go in two directions, just by the pure/impure idea. The pure on the inside, impure on the outside, where the pure is sending instructions to the impure, which executes them. That would be basically developing an effect system. Semantically that is wrong.
What you want to do instead is the opposite. The driver is a very simple script. You could call it procedural. It's just, "Do this, then do that, then do that." The bulk of the important logic happens in the domain code. Again, make sure it's a domain idea. It's more about the domain idea than about the pure versus impure parts of your code.
I hope that helps. If you want to ask more questions about this, I feel like this is a really important thing that I missed in the book because people are asking me a ton of questions about it. I didn't see that this would be such a confusing thing.
I'm not suggesting you develop an effect system where your functional core is driving the interaction layer, the impure part. I'm not suggesting that at all. I'm suggesting the opposite. That the interaction layer is driving, but the important decisions and logic about your domain are in functional core.
OK, thank you so much. Please buy my book. I would really appreciate it. It would help support me and it took a long time to write. I don't know if I'll ever recoup all the money that [laughs] I could have...The opportunity cost. The money I could have earned if I hadn't been doing the book.
It would really help me feel good -- even if I'll never recoup that money -- just to know that people like the book. Send me a message that you like it. Great. My name is Erick Normand. This has been my thought on functional programming and as always, rock on.