DEV Community

Cover image for Onion Coding: Programming in Layers
James Turner
James Turner

Posted on • Updated on • Originally published at turnerj.com

Onion Coding: Programming in Layers

If you've come wanting to know interesting details about that well known "onion project", I am sorry to disappoint. Instead, this article will be talking about my experiences structuring code into layers as well as touching on some well-known patterns.

This article isn't about what is the best way because I don't think there is one "best" way and even if there was, I am not smart enough to know what that is. What I hope you gain from this article however is ideas for how you could structure a current or future code base you work on.

I'm going to focus on C# however the core idea of separating and abstracting code can be extrapolated to any other programming language.

Separation of Concerns

This is the more "official" term of describing splitting code into layers. The goal is that specific tasks ("concerns") are separated from each other like business logic and presentation or data access.

A simple way of looking at it is: Does X care about the implementation of Y?

  • Does business logic care about the implementation of data persistence (eg. MySQL vs MS SQL vs flat files)?
  • Does "presentation" care about the implementation of business logic?
  • Does "presentation" care about the implementation of data persistence?

If you answered yes to any of the above, you might have some issues with your code...

Interfaces, not implementations

With the examples above, it is best to remember the question refers to "implementation", not "interface". If to your code MySQL and MSSQL had exactly the same interface (say LINQ in C#), why should any other part of your code depend on whether it is MySQL or MSSQL?


class Person
{
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string FavouriteColour { get; set; }
}

//An example of a query using LINQ
//No worry about MySQL or MSSQL, just my query in C#
var people = myDataContext.People
    .Where(p => p.FavouriteColour == "Green")
    .OrderBy(p => p.DateOfBirth)
    .GroupBy(p => p.Name);

Enter fullscreen mode Exit fullscreen mode

When you start heading down this path is when topics like dependency injection get brought up. Dependency injection can allow you to leverage interfaces to a greater extent and have one common root (aka. the "composite root") define what the implementations are and how they are constructed (eg. specific configurations for a specific class).

For example, you might be using EF Core which can have a variety of database providers. Your business logic might reference EF Core but doesn't know if that is MSSQL, MySQL or another provider. The composite root is what sets the provider and other things like the connection string.

James Hickey did a good article on Dependency Injection in .NET Core recently which is worth a read.

You could argue that you're not going to do something drastic like switch what database provider you will use to back your application and you're probably right. That, however, still doesn't mean separating something like business logic from your database implementation isn't a good idea. For example, being able to mock the persistence layer for testing your business logic can be extremely useful.

With all that said, let's start looking at some specific examples of layers in an application and how we can separate various concerns.

MVC as easy as 1-2-3: The Presentation Layer

Model-view-controller (MVC), maybe you've heard of this one, maybe you haven't. My short description for it is that a user interacts with a controller which does some "work", a controller then creates an output model and a model helps generate a specific view in return.

Wikipedia Diagram of interactions in MVC
By RegisFrey - Own work, Public Domain, https://commons.wikimedia.org/w/index.php?curid=10298177

This pattern is fairly commonplace for websites in one way or another - whether they use that terminology or not is a different matter. It has been primarily a server-side pattern however with more advanced client side applications being built, this pattern can be seen there too. The view though, it doesn't technically have to be web-related at all - it could be any method of displaying an output for your program. My experience though has been really only web-related on the server-side (eg. ASP.NET and a few PHP frameworks).

While there is a coupling of sorts between these 3 sections, it can be seen as generally separating concerns. For the most part, the controller is taking directions to user actions and coordinating what happens. The controller cares less about that extra <div> you threw into the view or the larger font you defined in CSS. It does however care about forming the model that will be passed to the view.

It isn't a perfect solution which separates every concern but overall, it is a useful pattern when done right.

In small applications, you could put all the business logic for actions into controllers. It is a testable solution (if you like calling controllers in tests) but it doesn't scale well when there is a lot of shared code. You could put that in an additional coordinator/handler/controller/manager class but is that the best option?

Maybe?

Taking care of business: The Business Logic Layer

Bachman-Turner Overdrive - Takin' Care of Business
Fun fact: I have no relation to the band...

When you start thinking of MVC only as a presentation interface, you can start structuring a new layer where you can actually put shared business logic. This is something to consider when you application itself gets bigger and you potentially might have multiple interfaces to the same business logic. This also can help define more precise unit testing rather than implementing it through MVC controller actions.

My approach for this was inspired by domain-driven design and the "Unit of Work" pattern. They are both definitely worth a look if you are in the process of structuring your code. I will cover "Unit of Work" a little more later in the article.

Let's look at an example and the how/why for moving that out of the typical MVC layer into a more business-logic layer.


interface IAccountService
{
    UserAccount Login(string emailAddress, string password);
}

class AccountService : IAccountService
{
    ...

    public UserAccount Login(string emailAddress, string password)
    {
        var account = Context.Accounts.Where(a => a.EmailAddress == emailAddress).FirstOrDefault();
        if (account == null || !account.VerifyPassword(password))
        {
            throw new AccountLoginException("Incorrect username or password");
        }
        if (!account.IsRegistered)
        {
            throw new AccountRegistrationException("Registration has not been completed");
        }
        return account;
    }
}
Enter fullscreen mode Exit fullscreen mode

A login-flow might not seem like a typical type of business logic function to bring to another layer as if you only have a website, a login-flow only appears once. The biggest benefit for bringing it out is that we are looking at the login rules abstracted away from handling the response to the user. This makes the code more modular and more easily testable.

Let's look at a few specific parts of the example:

  1. Logging in doesn't give away if the user or password is wrong
  2. Logging in does give away if the account registration hasn't completed
  3. AccountService implements the IAccountService interface

Logging in doesn't give away if the user or password is wrong
Like you would typically find when logging into websites, they normally say something along the lines of "invalid username or password" to increase privacy and security. You can't scrap users who have accounts or once you have an account, brute force a password.

By making this rule be at this level, we are saying no consuming code can ever know whether it was the username that was wrong or the password. We signal to the consuming code (in this case, via an exception) that this failed but we aren't responsible for handling how that gets handled to the end user.

Logging in does give away if the account isn't registered
This might seem counter intuitive given the first item however the critical point here is that it does this check after confirming before the email address and password. To know both details, you basically have permission to know whether you're part way through registration.

Notice though that the code doesn't do something like try and redirect the user to continue the registration? That is because we don't know what is consuming this code. If we wanted to perform a redirect, we'd potentially need access to the HttpContext but if this was a locally-run CLI application, that wouldn't make any sense.

This function doesn't even keep track whether the user is logged in or not because that is not it's concern.

The separation of concerns here, like the first item, keeps check login away from handling user response.

AccountService implements the IAccountService interface
This goes back to the Dependency Injection talk before, we might have an implementation like above for a website, called from a MVC controller. We might also have an implemented for a CLI application, which does an API call to said MVC controller.

This shows how DI can help separate concerns by having the consuming code not be aware of the specific implementation.

At this point, we have talked about both presentation and business layers, now to explore the world of data persistence and how we can have even more layers of code.

Persistence is the key: The Data Layer

When you are looking at abstracting data, one of the common phrases you might see is the Unit of Work and/or the Repository pattern.

Repository Pattern

This pattern involves a "repository" class per business object and is responsible for interacting with the data store - whether that be a database or a flat file. This class would typically have your general CRUD operations though you could have specific functions like "ListActive" for listing "Active" records only.

Unit of Work

This takes the Repository pattern one step further, wrapping repositories to be around a single type of transaction. Let's say for example you had a customer placing an order, a "Unit of Work" might both place an order as well as reduce current stock counts accordingly.

You might be thinking that Unit of Work is more business logic than data and you wouldn't be wrong in thinking that. In some ways, we are implementing business specific decisions like the stock counts example above. In others, we are just trying to ensure data consistency. I personally see it more as the latter.

In C#, you may have dealt with both of these patterns before if you used Entity Framework. It uses a DbContext (which can be seen as a Unit Of Work) with a series of DbSet properties (which can be seen as Repositories). With this in mind, it can seem somewhat redundant to implement the same patterns on top of it (personally I wouldn't) but if you wanted to abstract out Entity Framework from your business logic, you could wrap it again.

Opens a box, which inside is the room with a box, opens that box which inside is another room, recursively
Just need to wrap it in one more layer...

Wrapping it again does have its benefits for testing as it can more easily allow you to mock the Unit of Work. However this is where we need to start thinking whether continuing to abstract the code base is going to make it better or worse for maintenance.

Abstract Thinking: Is there such thing as too much?

It depends...

There is a lot of benefit to abstracting code and separating concerns including increased ability to test specific concerns and flexibility of the overall system.

There can be downsides though that your code base grows by several extra classes per business object, you might have some unwieldy composition root (if you use DI) and/or you might end up with 100s classes and functions with just 1 or 2 lines of code in them.

Some people might see that as a great thing but I am not one of them. I do like some meat to my functions so while I will abstract things away, I do think about whether a specific abstraction and separation makes the code easier or harder to manage.

Your Mileage May Vary

At the end of the day, the level of separation and abstraction depends on many things. This article isn't to tell you which way is best, just to give you some food for thought.

Top comments (0)