NDepend

Improve your .NET code quality with NDepend

onion_architecture_layers

Onion Architecture: Going Beyond Layers

So you’ve read about the Onion Architecture and think you get it. There are layers in this architecture. They’re onion-like. And they make your code better.

But how? And what do you put in each layer? And how the heck do you get your repository to be on the outermost layer of this onion?

How Do I Slice This Onion?

I wasn’t sure about the onion analogy at first. If you require wedged or diced onions, you should start with a cut through its root and that top thing that sticks out and grows when you don’t use the onion fast enough. However, if you require an architecture analogy, you slice it horizontally through the center. If you cut it correctly, you’ll see circular layers, like tree rings. Unfortunately, a tree ring analogy would never work as that would require adding a new layer to your application each year. So maybe the onion analogy isn’t so bad—as long as your onion is small enough. Now let’s look at how this architecture came about.

Jeffry Palermo coined the term Onion Architecture in a blog post back in 2008. His goal was to remove the problems that more common architectural practices, like the layered architecture, faced.

While writing his thoughts, Palermo also defined the key tenets of Onion Architecture:

  1. The application is built around an independent object model.
  2. Inner layers define interfaces. Outer layers implement interfaces.
  3. The direction of coupling is toward the center.
  4. All application core code can be compiled and run separately from infrastructure.

With these tenets, each outer layer is only allowed to depend on more inner layers. So the UI and database layers can depend on your domain logic and business rules, but not the other way around. All the coupling and dependencies flow down toward the center layer of the onion.

Invert the Layered Architecture

Speaking of layers, you may have heard that Onion Architecture is an inversion of the layered architecture. But what does that mean? A while back we looked at layered architecture and how it can still be a good approach for some applications.

Like the Onion Architecture, the layered approach requires coupling from one layer to another. However, with the layered approach, your database ends up being the base of your application. Let’s take a further look at the coupling in the layered approach.

For example, in a simple three-tiered architecture, the UI layer couples with the service layer and therefore the database layer. So then everything in your application is dependent on your database layer. That might seem like no big deal, but let’s consider how often companies and applications make changes to their database. They can go from having all the logic in stored procedures to having it all in their ORM. They could also change their database vendor, their ORM layer, or even go from a relational database to a NoSQL solution. And then it could all be switched back to RDBMS when you realize that NoSQL is there to solve a problem you don’t even have. And none of these changes have anything to do with your core business functionality.

So how do we invert the layered architecture? Well, we take the bottom layer and put it on the top with the UI. Why does putting the database up with the UI make sense? Just like your API should be dependent on your core business logic, so should your database. Use the outer layer for things that change.

Now let’s take a closer look at those layers.

Deconstruct the Onion

First, I’d like to fill you all in on a fun fact. There are a lot of recipes online for deconstructed onion rings. Yes, really. Thank your closest hipster and then go try to figure out exactly what that’s supposed to be. Because after minutes of searching, I’m still not sure.

Now that I’ve gotten that out of the way, let’s look at the various layers of the Onion Architecture. Most descriptions start on the outside, but I’d like to start in the middle. That’s the most important part of our onion and everything else builds on top of it.

Domain Layer

In the center of your onion is the domain layer. It defines the state and behavior of your application. When looking at the domain classes, you should be able to determine the purpose of your application.

Even after that simple explanation, it might still be hard to understand what the domain layer actually did. Is it just POJOs? Do you have to do some coding and mental acrobatics to avoid bad coupling? Do you fill it with a bunch of interfaces and call it good?

Let’s consider an example of a sales order system. And let’s say that our system requires the ability to retrieve an order by order number. The domain defines the concept of an order and an order number. Additionally, It defines the interface for this behavior. Now the actual data access implementation itself exists in the outer layer. And it doesn’t matter if this implementation involves a database repository, a file system, or a guy named Stan who types out the order details from memory. How we do something is not in the domain. It is only what we do.

To sum it up, the domain defines the state and behavior of your application. So while shipping an order is a behavior for our system, interacting with the shipping company system is an implementation detail that should be in the outer rings.

Domain Services Layer

The domain service layer defines domain-specific events. Again, we might not define the implementation. What are domain-specific events? These are events that your service layer will define interfaces for, like completing orders, shipping orders, returning orders, and so on.

We also might see some implementation for the core domain interfaces. However, this will only be implementation that doesn’t require external systems and infrastructure.

Let’s look at an example, The domain layer defines functionality that’s core to our sales order system. So perhaps one required function is calculating tax on an order. Our domain layer would define all the entities required for that calculation, like price, quantity, and state tax. It would also define the interface that’s required to calculate it. Now the actual logic for implementing the tax would not be in the domain. That implementation would be in higher layers, like the domain service layer.

Application Services Layer

So where do we keep the logic for implementing the interfaces in the domain service layer? It’s here in the application services layer. Well, again, only if it doesn’t rely on infrastructure. In fact, many applications combine the two service layers into one.

Infrastructure Layer

Along the outer ridge of your onion is your infrastructure layer. It’s how your application communicates to the outside world, whether it’s the data access logic or API calls. Palermo showed three main types, though there may be more.

  1. Test: There should be no code in the application that’s relying on your tests or testing framework.
  2. Infrastructure: Yes, this has the same name. This is your file system, database, and other external systems
  3. UI: The UI can be a GUI, an API, or another defined interface.

Anything that’s interchangeable, replaceable, or external to your system lives in this layer.

And now that we know what we’re dealing with, why do we care?

How Can You Avoid Recipe Disasters?

There are some common mistakes people make when implementing the Onion Architecture.

Don’t Let Your ORM Leak Into Your Domain

Using tools like Entity Framework in the domain layer, or your framework’s entity class objects in the domain layer, are coupling to an ORM. The domain models should be POJOs. In fact, your domain model shouldn’t even know what framework you’re using. Let your domain layer know about implementation through exceptions.

Let’s look at our order tracking system again. In the domain layer, you have an addOrder() function. The implementation in the data layer adds the order to the database. But there’s a problem—that order number already exists. So then your database throws a fun SQLException with a cryptic number of 2627 that you catch in your domain layer. Do you see the problem? If your domain layer has to know about a particular database’s primary key exception, it’s now reliant on the data layer, which is bad. So what your domain layer should do is define its own OrderAlreadyExistsException. The database or repository implementation layer then converts its primary key exception, or whatever, to the exception that’s been defined by your domain.

Identify All Components That Are Infrastructure

We’ve talked a bit about what the infrastructure layer is: the database, the “typical” infrastructure components like database and a filesystem, and tests. But what else is there?

One that I didn’t see mentioned is logging. Logging is also in the outer layer. Treat it as such by using interfaces within your core application instead of a logger implementation. There are tools available that can help. For Java, there’s SL4J, and for C#, we have Common Logging.

Something else you may not realize should be pulled out to the outer layers (or as far as you can get it) is your framework. Whatever framework you’re using, you need to pull it out of your domain. This will help to keep things testable (less mocking) and also help when you decide you want to use a different framework a few years down the road.

Have We Gone Beyond the Layers?

Now we’ve seen the types of layers there are and what they’re used for. But you won’t be a pro at this starting out. You’ll make mistakes and learn from them. The most important piece is to start using some of these lessons to build your systems. Then you can find out what value it provides. And you’ll be more comfortable with pulling things out in ways that make your code more modular and maintainable.

Contributing Author

Sylvia Fronczak is a software developer that has worked in various industries with various software methodologies. She’s currently focused on design practices that the whole team can own, understand, and evolve over time.

Comments:

Leave a Reply

Your email address will not be published. Required fields are marked *