On the Structure of (object oriented) Code Solutions: Onion Architecture

Navigate through the wild world of C# (.NET, .NET Core) and Java solutions in your organization or the public GitHub repositories. It’s like wandering into a virtual jungle, where things can get pretty wild in there. You’ll stumble upon projects and folders with names that seem to have been chosen explicitly to confuse a new developer. Some of them are packed neatly inside folders, while others seem to have escaped and gone rogue. It’s a real party of chaos and creativity out there! You’ll find some brave souls attempting to establish order with their conventions. Some try to divide the solution to BusinessLogic, DataLayer, and “Shared”…but then we see some”BuisnessLogic.Shared” or “DataLayer.BusinessLogik” sitting somewhere around…

All of these have been and still are confusing for me. I wanted something clear and simple, and during the last four years, I have come up with a structure which, at least from my experience, is not only beautiful and simple but also helpful: It assists new developers in finding their way in the solution. I could use this structure for a wide variety of solutions: Classic Sql based MVCs, modern NoSql based micro services, and even a chatbot based on the Microsft Botframework! Let me explain how it looks like.

My starting point has been the great idea known as the “Onion Architecture” from Jeffrey Palermo. I always start my solution with a “common” project:

The Common One

All those helping classes and methods (which are not specific to any project) go in there. An example from asp.net core is my ExceptionExtensions class, where I have my string GetExceptionDump() method, and use it for logging all the time.

So why I do not create a nugget package and add it to the company’s feed and use that through all projects, you might ask? Because I want and love the flexibility. If some developer working on the project feels that the GetExceptionDump() method should accept a Boolean as input to control whether or not to include the whole stack trace in the returning string, it is fine. She just goes ahead and adds the new method to the ExcpetionExtensions class in the common project! No need to discuss with other teams which use the same common (nugget) package in their projects, and no need to convince the team responsible for maintenance of the common package, if there is any such team!

The Core One

The next project will be the core project, with only one project dependency, the Common, of course. It is important that we keep it this way. The core project remains independent of all other projects, besides the Common one. But what happens in the Core?

I am going to solve the main problem(s) here, while remaining independent (from everything like the specific type of database we decided to use, the secret vault service my company currently uses, and so on). That is, I add all sort of interfaces to the core, but only interfaces: An IRepository<Product>, another IKeyVault, another IFileStore, and maybe an ITranslateText and an IEmailService. Depending on the context and needs, I see what I need to be able to get the results. As mentioned, I only add the interfaces in the core project. For the main problem(s), I add the interfaces and I implement them, using the other interfaces: I get the ProductId as input, use the IRepository<Product> to get the product from the database, without being specific about how and why…then I do some other checks or calculations, probably using methods form other interfaces, maybe I need to save something back in the db using the IRepository again, and return the final result at the end. Remember that we get the implementation of each interface in the runtime using DI (dependency injection).

The Database One

Ok, so let us start with the “other” things…one of them is often the data base. I said I add the IRepository kind of interfaces in the core and use them in the implementation of my main services there. Now I add a project called DataBase, which has the Core as it’s dependency, and I implement the IRepository for the kind of database that we are going to use in it. In .Net Core, if we want to use the EF, I add the appropriate dbcontext classes here too.

The Other Ones

You need to communicate with another API in your company or from your client to send or receive data? Add the IOurCustomerApiClient to the Core, and implement it in its own project. You need to use Azure Blob Storage or Azure KeyVault? Same story. I put all Azure things (besides the database ones) in an AzureServices project. All these projects are only dependent on the core project.

The ApiBase One

Quite often we need more than one api for the solution, more about it below. To avoid writing the same code, like the MiddleWares, I’ll add an ApiBaseProject. It depends on all of the “Other Ones”.

The Api Ones

Finally we add some ways for others to use our main services. The “Api” projects are exactly for this purpose. They depend on the ApiBase project. I often realized that it is better to have more than one of them:

  • One for triggering and monitoring the long running tasks.
  • One for serving the SPA/UI designed only for the administrators.
  • One for serving the SPA/UI designed only for the normal users.

This allows to have separate CI/CD pipelines and deployment routines.

Please tell me your opinion about the above mentioned structure, and how you structure your code.

Published by szarghani

I am a C#/Java developer. I live and work in Germany.

Leave a comment