In the onion architecture, how do you make business decisions that rely on information from actions?
This is an episode of Thoughts on Functional Programming, a podcast by Eric Normand.
I've gotten several questions about how to do X or Y in the Onion Architecture. It seems like giving the architecture a name has miscommunicated how simple it is. It's just function calls that at some point are all calculations. In this episode, I try to deconstruct what makes the onion architecture work. Spoiler: it's just function calls.
Eric Normand: In the Onion Architecture, how do you make business decisions that rely on information from actions? I get this question a lot, especially when I bring up Onion Architecture in more of these episodes. In this episode, I am going to answer it for everyone. My name is Eric Normand. I help people thrive with functional programming.
The Onion Architecture is a way of structuring your application with actions on the outside. These are called the interaction layer because if you're interacting with the world you really have your enactions. You're receiving requests from the outside. You're making API requests yourself.
You're reading from the database. You're doing a lot of IO, having effects on the world or sending emails. You're making lights blink. Whatever your software does. That's all in the interaction layer.
Inside you have a nice, pure set of layers that are all about calculations. I like to divide them up in a certain way. I like to put the business rules as my first layer inside. Inside of that, a domain layer.
Both of those, it doesn't matter how you divide it up, especially for this discussion. They're calculations. They're pure. They are not based on stuff from the outside. They have no effect on the outside. They're like little brain that you can give it questions and it will answer their questions. It's making decisions, basically.
You got this calculations making decisions, business decision, even simple domain decisions. The actions are doing stuff like fetching stuff from the database. Sending data to an API.
Here's the question that I get a lot, "If you've got the Onion Architecture, how do you make decisions that should be calculations like how many times to retry an API? If it fails the first time, do you retry?" That's a decision your software has to make. How do you have a calculation that decides that it needs to have more information from the database?
It says OK. I've done a bunch of stuff. Now I know, I need this more information. It can't get it itself. How does that information get up to the higher layer so that the higher layer can get it and then give it back to it? It's weird. It starts to sound like a really difficult problem.
I said, "I've gotten this question several times in different forms." It's a thing that I've caused a confusion I've caused in how I've explained it. I'm using language to try to talk about them as separate layers. People aren't really used to thinking in layers.
I'm going to try to pick it apart and put it back together with a new explanation. When I talk about layers, I'm talking about functions in your language, they're either calculations or actions. Functions calling other functions. Function A calls function B.
There is a relationship there. If you draw all of the relationships between functions, and what functions those functions call, then what functions of those function calls. You make sure all of the arrows are pointing down. You have the stuff that nothing calls up at the top that would be like your main.
Then you have the stuff that calls, but nothing else called and you can just arrange them all. At some point, you could draw a line and say, "Everything below this line is calculations." Because a calculation cannot call an action. If a calculation called an action, it's not a calculation.
By definition, calculations cannot call anything above that line. You could draw a line and say, "This stuff down here is all calculations." The stuff above is all actions.
That's what I mean by the layers. It's all just function calls, it's not like some kind of protocol for communicating and getting information like, "Here's a decision I need you to make," so you give me the answer, and then I ask you what I do with that answer.
It's not that, it's just function calls. Let's look at the two examples I gave, these two questions that were asked. If you have to do a retry, you make an API call, and it fails, it times out, you don't know what happened. I'm going to retry it. How do you decide whether to retry it?
Let's say your rule in your system is retry it three times or retry it two times. You try it once, and you have two more times to retry. That's really just a less than. You keep track of how many times you called it, and then you see if it's less than three. If it's less than three, then you'd keep trying. Then you decrement or whatever, and increment.
That less than is a calculation. It is not named, and is probably in line right in your action. That is a calculation. It is a decision being made. You could, if you wanted to, say, actually this less than sign, this less than operation, less than three, is a business rule.
I probably wouldn't call it a business rule. It's more like an architectural rule or system integrity rule, something like that. You could say, I want to name this function that will decide, based on how many times I've already tried the API, whether I should try again.
Just a simple Boolean. It's basically just the body of the function. It's just less than. Less than three or whatever number you choose. You could say that's a business rule. Put it in there. The action is just calling this, like function called Retry Question Mark or Should Retry. This is the name of the function.
You notice, this is the name of the action calling a calculation. That's it, that's all it is. The action is in the interaction layer, and the calculation is in the business layer.
What if you're doing something, and you're doing this big calculation. You've managed to turn it into a nice data pipeline. At some point in the pipeline, something says, "Whoa, I need more data from the database. I need this record," or, "I need this whole set of records from the database to continue working." What do you do?
Again, when I read the question, it sounds like the same thing, where people are thinking like, "This interaction layer is telling the business layer to do all this work." Then, somehow, the business layer needs to communicate back up to the interaction layer, and it's going to go fetch something. There's this back and forth, back and forth communication.
I don't know what people are thinking, but it sounds a lot like an object-oriented mindset, where you got this two peers that are communicating.
It's server, just peer-to-peer communication, with this protocol of like, "You tell me what data you got, and I'll start calculating. When I need more, I'll tell you that I need more, and then you'll fetch it for me, and then I'll keep going, and then I'll tell you I need more."
That is not what I'm trying to get at. When I have taken the code that people have given me, example, this is really hard to do on the Onion Architecture. Basically, all I do is I move stuff from this big action. It's like 20, 30 lines, and I just move things. I said, "Oh, that could be a calculation. That's like a business rule, and this is a business rule." I just move it into calculations.
Then the action just gets shorter, because now it's just calling these name functions instead of all this inline code. A lot of it is moving into other actions, by the way. It's like threading it through.
OK, we're going to fetch this thing from the database, and then past that to this calculation, it's going to give us an answer. Then we take another thing from the database, and we pass it to the next function with what we already had. That's going to do some other calculation.
It looks like regular code. It just looks like normal code. I feel like by naming the thing, like Onion Architecture, I've somehow confused people that they think it has to be much more sophisticated, complicated than it has to be.
This is the way I see it. A calculation can only make decisions based on what it knows, what it has been passed through the arguments. It can't say, "Oh, I need more data." Whatever has to decide, "This thing needs more data," it's not a calculation. It's an action in an upper layer.
What comes out is that we're doing this because of efficiency, because we might not need to fetch that huge data set from the database. We won't know until we're halfway through the calculation whether we're going to need it or not.
That's cool. That's a different problem. Now, we're talking about, "Are we doing a lazy thing?" Or, "Does this calculation really have a natural point, a break point, where it's really two calculations that can be called separately, and results from the first thing can be threaded into the second part?"
Is that what's going on? The idea that this calculation gets to a point says, "OK, I need more data." The laziness might solve it. We've talked about that before, in previous episodes. We constructed a delay. The delay, it's a suitable thing. Because the calculation is still pure, even though now by triggering this delay to be realized, it is fetching data.
That logic was injected in. It was passed in from the outside. In fact, I've seen Onion Architecture implementations that get passed a function, and then that function just returns the data. You could pass it in a function with dummy data that just gets returned, or you can pass it a function that will fetch the data from the database.
Does that turn that calculation into an action because it is now fetching from the database, even though it doesn't know? That is a very philosophical question that you're going to have to draw the line somewhere, where you feel comfortable with, I would be very comfortable with that kind of thing. That's up to you.
I don't know if I've really done my goal, [laughs] achieved my goal of deconstructing this, and really framing it back in terms of function calls.
The Onion Architecture, it's maybe more of just a way to look at it, that you don't have to have everything in a big set of actions. That you can push business rules down into calculations, down the layers.
Remember, if all of our dependencies, all of our function call lines are pointing down, that means calculations...They can't call up because they're below the actions. That's what I mean by pushing it down.
We're taking these business rules, making sure that they're implemented as pure functions as calculations. They go into a separate layer that then gets called by the interaction layer. That interaction layer is stuff like, I got a Web request — that's an action. That depends on when, it's a timely thing. You also can't decide when you'd get it, it just comes.
I got a Web request. Now, I have to decide what this Web request is. I'm going to route it. The route, that's probably a calculation. It's going to take that path, and it's going to tell me something, like how this request should be handled.
I take that information, I know how it should be handled, still in the interaction layer. That means I had to call this handler. This handler needs XYZ to be called. It needs a little bit more data. It needs the user information, the session, that kind of stuff.
It adds that in, and then the handler gets called. Handler is probably still part of the interaction layer, it still might fetch stuff from the database. This thing is going to fetch that from the database, make a big decision or several small decisions from the business rules, package it up as a response, and then send it back out.
It's all function calls. The handler is calling calculations, and then coming back. I look at it like, you don't need to do anything special, except just make sure that at some point, you do have all calculations going down.
That you don't have a thing where this business rule that is deep down in the...The call graph is going to fetch out to the database somewhere. That's all it is. That should actually be up at the top, and make sure that stuff is pure. That's all it is.
I hope this hasn't been too mystifying. I fear it has been. If it has, get in touch with me. I want to talk about this in a way that's more understandable. If you have a better way to explain it, if I'm confusing you more, send me examples of things that you don't understand how you could turn into an Onion Architecture.
You can get in touch with me by going to lispcast.com/podcast. There you're going to find links to social media like email, Twitter, stuff like that — whatever you think is best for communicating with me, based on the length. Probably if you got some code, it's best to go by email, not Twitter, but you decide.
You'll also find links to subscribe to this podcast. You'll see all the old episodes with audio, video and text transcripts, so if you need to go back and binge-listen, binge-watch or binge-read, it's there. I was looking at it the other day. I have 139 episodes, so this is 140. That's quite a number.
This has been my thought on functional programming. My name is Eric Normand. Thank you for listening and rock on.