How to apply the Onion Architecture
This is an episode of Thoughts on Functional Programming, a podcast by Eric Normand.
I got a lot of questions about how to apply the Onion Architecture to particular situations. In this episode, I try to answer it with a specific example.
Eric Normand: I answer some tough questions about the onion architecture. Hi, my name is Eric Normand and these are my thoughts on functional programming. This is season two and I hope to up the production quality that I had in the last season.
Let's get right into it. A few episodes back I introduced something that's called the onion architecture. I introduce it, meaning I explained it. Just real quick, the onion architecture is the layered architecture where you have your core domain implemented in a functional way in the middle.
It's layers like an onion. Around that you have your business rules, also functional. Then finally, the last layer is this interaction layer. This is the layer where all the actions happen. This is the layer that talks to the database. It gets Web requests and makes other API calls.
This is an architectural pattern that you can use to have a functional-style architecture. After I posted that, I got a lot of variations on the same question, which is, "How do I make decisions in my domain model that then result in taking different actions in the outer layer?"
The idea is, if you weren't going to architect this, let's say you just had a very basic, straight forward implementation. It's imperative. You would make an API call. Based on the results of that, you make some decision. Then you either call API A or you call API B. That's the end. The logic of what you do is mixed in with the actions that you take.
How do you extract that out into something that you can call an Onion Architecture? Right. That is the real question that people have. I have to say, I got a couple of examples of what people wanted to turn into an Onion Architecture.
My first thought was, there's not enough logic in it. There's not enough business rules. There's not enough domain in it to warrant coming up with different layers. Then I thought, "No, these are just simple examples that they're giving, so let's do it right." Here's an example that someone gave me. I should look up the name so I can reference them.
This is Andrew. Andrew, thanks for the question. In his example, you are implementing a web endpoint. This endpoint is for information about music albums. In the endpoint, you want to include images of the artists who worked on the album.
There's some constraints. You need to have as many images as you can, up to five. Things like that. The problem is, there's a lot of logic about...For instance, if there's more than one artist on the album. It's a compilation CD and you have a song from this artist and a song form that artist, you want to have one image from each artist. Not five of the first person.
The rules are based on...you read the thing in from the database, you make some decisions about where to get these images from, and then you go and find them. You do another database query to get the images. Then you put them together into a JSON and you send it back.
This is an example of something that, on the surface, if this were the real thing that I was implementing, I probably would put all of that logic right in the outer layer. Because it's not that much.
The logic is something like, "Is it greater than five images?" [laughs] Then there's not much more to it. I'm going to run with it. I'm going to turn it just as an exercise to explain it. I'm going to turn this into an onion architecture.
I'm trying to explain this in audio. That less than five, greater than five whatever it turns out to be, you could consider that a domain. Sorry, a business rule. The business rule is a valid artist or a valid album response has at least five images, something like that. That could be the expression in English of your rule.
Then in your business logic, you have a function called valid album response. It could be true, false or it could be something like true, then return or a list of problems. Now, this is convoluting it because the rules aren't that complicated, but if you did have a lot of rules that you needed to apply, this thing could tell you all of the problems.
Now, these are business rules. Maybe those...what is the domain here? Is the domain album information and where to get images about different artists? I think that's stretching it, but you could consider something like that, like album information, the repositories of images, of artist information, that kind of thing.
If you have an error in the business rule layer that says not enough images, then that kicks off at the outer layer a thing that says, "If I have not enough images, here's how I rectify that." Now, again I wouldn't do it this way, because that's such a very small bit of logic. There's not much of a domain to build up there. There's not much material there to work with, but you could imagine if the domain were more convoluted like a real-world domain would be, that you would have this sort of business rule layer that could decide, "Is this something I can respond with? If not, what are all the problems?"
Then your outer layer knows how to rectify those problems by asking the APIs or the databases of images, et cetera how to do that.
When you do architecture, you're taking on...there's a trade-off. You're adding complexity at the beginning. It's a known complexity. It's a known cost. This is how we're going to do our architecture. That's how we're going to structure our work, our code. This is the patterns that we're going to use.
Then you get a benefit. Depending on how much code is going to fit into that architecture, you're going to get a different benefit. With a very simple system, it's probably not worth architecting just to be honest. If it could just be an imperative, "Fetch this, look at the response. If it's good enough, send it. Otherwise, add more images." That, to me, is not even that much code to write that. At some point, as your system grows, you're going to think, "OK. I need some kind of organizing principle to make this less painful, less of a mess." Once you're at that point, that's where architecture comes in.
This is a valuable way to architect it because, what happens is, let's imagine your code was really long. It was 200 lines to make this response. You're making Web requests. Then you're doing all sorts of inline calculations. Then you're making another request somewhere in the middle. You're going to wish that you had it in pieces. Then where do you put those pieces? How do you cut those pieces up?
Those are the architectural questions. Just to recap, the onion architecture is saying you want three broad layers. The domain layer is where you encode the problem domain but without any of your own business's policies, rules, and regulations that your business imposes. Your business might say, "The albums we deal with are only swing albums from the 1950s." That might be a business rule.
That doesn't mean that your domain has to know about that, especially since what if your business expands? Now you have all this code that you can't use because it's all assuming swing from the 1950s. It's a silly example, but I hope the point is clear. That's your domain code. It's how do we see the problem without any influence about practical business things?
Then you have your business rules, which are things like, "Ah, we want to return five images in our response." I'm trying to come up with business rules that make sense in this domain. Anyway, there's business rules that are almost proprietary. "This is how we run the business."
Then there's this outer layer, which is simply for communicating that will query the business layer as you would query a third-party API. It queries the database. It takes that response. It asks the business-rule layer, "What should I do with this? Is this good enough? Should I return it?" There's a little logic in the interaction layer, but it's very much coordination.
It's similar to the model-view controller architecture, which, if you look at it, the controller is where all of the coordination happens. The view is sending events to the controller. The controller is saying, "OK. When I get that event, that means I need to call this on the model. Take the results. Now it goes to this view because it's going to change that." It's orchestrating everything.
All the real action, all the real change and important stuff, happens in the model or the view. The view is showing the user, and the model is maintaining the business rules and the consistency of the state. The controller is just saying, "OK. I need to do these three things and then send that off to the view. I choose this view." Then boom. It's good.
That outer layer is very much like that. It's saying, "OK. I got this Web request. I know I need to call this business-rule function, which takes something that I get from the database. I got to get the database, query the database, ask the business-rule layer the question I have." Then it will return the answer. Then that I can use. I make another decision. I branch.
I either do this or I do that. Then I send off the answer. That's the idea. All right. My name is Eric Normand. Please subscribe. This is the first episode of Season Two. Season One was very impromptu. It was me walking around holding my camera or driving around. Sometimes there would be bad audio, bad video. I'm trying to fix that.
I constructed this little corner of my office that will be my little recording studio. These are whiteboards. This table is also a whiteboard. I'm looking forward to finding creative uses for that. You can contact me by email. I'm email@example.com. You could also find me on Twitter, @ericnormand with a D.
Just search for Eric Normand Clojure on LinkedIn if you're into LinkedIn. You can follow me there, too. Don't forget to subscribe. I will see you next time. Bye.