Solution & Project Structure of a Loosely Coupled Monolith

Solution & Project Structure of a Loosely Coupled Monolith

Here’s how you can create a solution and project structure to develop a loosely coupled monolith using a .NET Solution with C# Projects.

Each boundary is in a solutions folder with 3 projects. Implementation, Contracts, and Tests. All of which are class libraries. The two top-level executable projects, AspNetCore and Worker are console applications that reference the implementation projects.

Loosely Coupled Monolith

This blog post is apart of a series of posts I’ve created around Loosely Coupled Monoliths.


Check out my YouTube channel where I post all kinds of content that accompanies my posts.

System Diagram

Here’s a diagram of what the Solution and C# project structure should reflect.

Solution & Project Structure of a Loosely Coupled Monolith

Bounded Context

The middle row of the diagram has 3 boxes that represent a boundary. These boundaries should define the set of capabilities of your system. Each boundary contains 3 projects: Contracts, Implementation, and Tests.

Solution & Project Structure of a Loosely Coupled Monolith

In my example above, there are 2 boundaries: Sales, and Shipping. For each one, I’ve created a solutions folder which the 3 respective projects for each boundary inside. All 6 projects are C# Class Libraries.


Each implementation project (Sales & Shipping) will only reference other Contract projects. They will never reference another implementation project.

Solution & Project Structure of a Loosely Coupled Monolith

The example above has the Sales implementation referencing the Shipping.Contracts.


There are two top-level projects that are our executable projects. AspNetCore and a Message Process (Worker).

Top-level projects will reference the implementation projects to expose their capabilities


This top-level project is an ASP.NET Core app. Which is really just basically a console app using Kestrel as the HTTP server.


The mentioned this project will reference the Sales and Shipping projects.

This is because each implementation project must define its own configuration. Things like Dependency Injection, HTTP routing, etc.


The above example is of an extension method for defining all of the types that needed to be registered with a ServiceCollection. In the case above it’s adding the Entity Framework SalesDbContext.

This can then be used in the AspNetCore’s ConfigureServices.



The Message Processor (Worker) is the other top-level project that is a console application. Its purpose is to connect to a message broker and dispatch the relevant messages to the implementation projects. I’ll cover message processing in other blogs/videos as the purpose of this post is simply the solution and project structure.

As you can expect, it will reference the implementation projects and use the extension methods just as the AspNetCore project does.

Generic Host

The Worker is using the Generic Host which is very similar to the WebHost from ASP.NET Core. It provides all the same goodness that you get with an ASP.NET Core app such as Dependency Injection and Logging.

In this case, I’m creating a HostedService which is a BackgroundService. This is ultimately where we would be communicating with a message broker to dispatch and handle messages within our Monolith.

Again, more on how that works in other posts (which will be linked at the bottom of this post when they are published).

Questions or Feedback

If you have any questions or comments, let me know in the comments of the YouTube video, in the comments on this post or on Twitter.

Follow @CodeOpinion on Twitter

Enjoy this post? Subscribe!

Subscribe to our weekly Newsletter and stay tuned.

Loosely Coupled Monolith

Loosely Coupled Monolith

With microservices all the rage over the past decade or so, there has been little attention paid to how to develop a Loosely Coupled Monolith.

Both microservices and monoliths have their strengths and weaknesses. The intent of this blog and video series is not to debate the use of one or the other but rather to illustrate how you can develop a loosely coupled monolith. What the benefits come from a loosely coupled monolith and some of the drawbacks.

Loosely Coupled Monolith

This blog post is apart of a series of posts I’ve created around Loosely Coupled Monoliths.


Check out my YouTube channel where I post all kinds of content that accompanies my posts.


Regardless of Monolith or Microservice based architectures, boundaries are critical to both. In Domain Driven Design, the concept of a Bounded Context is defined. I’d argue it’s the most important concept from Domain Driven Design. It’s driven by language and mutual understanding between business and software. I’ve written & talked about boundaries in my Context is King: Finding Service Boundaries series.

For a short re-cap, for me, boundaries are about ownership. A Bounded Context is about business capabilities and the data behind those capabilities.

It’s NOT about entities. It’s NOT about having an entity service that performs CRUD over those entities.

It’s about capabilities that will be owned by the bounded context. With that, you cannot have behavior without data.

In a solutions view of a bounded context, I like to think of it as having 3 projects.

Contacts project contains things like interfaces, delegates, and DTOs (Data Transfer Objects).

Implementation project contains all of the code for the actual implementation of your bounded context.

Tests project is for well… tests.

Project References

With any large monolith, you’re going to have many bounded contexts. The key is that they are silo’ed from other bounded contexts implemenation.

Any implementation project will not reference the implementation of another bounded context. The only reference it can have is that of other contract projects.

An implementation project will only couple (reference) to contacts projects because that’s where our DTOs that represent messages & events live. DTOs are nothing more than data buckets. The contract projects contain no actual logic.


Since each bounded context is the owner of its own data, there is no need for a shared database.

Each bounded context MUST have its own database which it is the owner.

No other bounded context will have access to this database or any type of data access layer such as Entity Framework DbContext. Each context has access to its own database and that’s it.

Top Level Entry Points

There must be some top-level entry points to our application. These are the actual executables that are running in service, in a virtual machine, or in a container.

In the example of this monolith, I’m assuming it’s a web application that is either an HTTP API or is serving HTML as a server-side web app.


There is a single project that is the ASP.NET Core Host. Basically this hosts the WebHostBuilder and the ASP.NET Core Startup class to configure the app. It will reference all of the bounded-context to provide them with a means to expose their HTTP routes (MVC Controllers or Endpoints).

ASP.NET Core is composing all the bounded contexts together.

Message Broker

Loosely Coupled Monolith

Bounded Contexts generally will need to communicate. This is done via events. This is no different than Service Oriented (SOA) or Microservices architecture that is event-driven.

This is where a message broker enters and also why the contracts project contains our DTOs that represent events.

Whenever a bounded-context has a state change that is derived from behavior, it can publish an event to the message broker.

Message Processor

Loosely Coupled Monolith

Another top level entry point is the Message Processor. This is the process that receives events/messages from the Message Broker and dispatches them to the appropriate bounded context that wants to receive them.

An event/message may be received by none or many different bounded contexts.

A bounded context that receives an event might then perform some state change and publish another event back to the message broker.

Loosely Coupled Monolith

This is really just an event/message driven architecture inside a monolith. It has clear boundaries between bounded context and separation of data.

In many ways, you could view a lot of the aspects of this architecture as similar SOA however the difference being that they are all in the same codebase and all hosted in the same top-level entry points (executables).

Benefits & Drawbacks

As with everything there are benefits and drawbacks to this approach here are a few of the bigger ones to note.


The benefits of this approach are that the same of any monolithic architecture. The code is altogether. All the source code is all within the same solution/repository.

If you want to refactor an event you can find all the different bounded contexts that use that event.

Simplified deployment (and local debugging). There are only two actual executables that need to be deployed (this can also be a drawback).

Lastly, if you ever want to carve off a bounded context and host it on its own, you simply do it. There’s no coupling preventing it. You simply copy the top-level entry points and only have it access the bounded context you’re extracting.


Single deployment. This isn’t a drawback on this specific approach but on monoliths in general. When you’re deploying, one bounded context can take down the entire application. Since ultimately all the code gets run together in the top-level entry points, there’s more risk during deployment.

You must be diligent in following the rules of not referencing implementation and not accessing another database of a different bounded context. This takes an understanding of all developers working on the system.

If your system becomes large enough, build times and deployment times can take a while.

Questions or Feedback

If you have any questions or comments, let me know in the comments of the YouTube video, in the comments on this post or on Twitter.

Follow @CodeOpinion on Twitter

Enjoy this post? Subscribe!

Subscribe to our weekly Newsletter and stay tuned.

CQRS: Refactoring Queries without Repositories

CQRS: Refactoring Queries without Repositories

This question posted on Twitter (and the subsequent thread) caught my attention. I had to pipe in with my own opinion on having queries without repositories.

To summarize, the question is about using Aggregate Roots with an ORM like Entity Framework. Should you eager load all navigation properties or alternatively I guess, use lazy loading?

My answer is if you’re only using an aggregate root for commands (to change state) and you generally have a higher read to write ratio, then eager load the navigation properties. As a general rule of thumb.


I recently heard about eShipOnWeb from Steve Smith. It’s a reference application using ASP.NET Core 3.1 using a layered monolithic architecture.

I decided to take a look around and try and refactor a spot that was using a repository for a query.

There is a query to return all of the orders for a signed-in user. It’s pretty simple but is a good example of it using the OrderRepository along with a specification to fetch out the relevant orders. From there it creates a ViewModel that is used in a Razor view.

Here’s the razor view for reference

Looking at the Razor view, you can see that it doesn’t use some of the properties on OrderViewModel. The OrderItems or ShippingAddress properties are never used.

The Repository with the specification is retrieving the entire Order Aggregate Root and eager loading the OrderItems. We’re fetching in a lot of data that we simply aren’t using.

Refactoring Queries without Repositories

The refactor is rather simple. Just remove the repository and instead inject the Entity Framework DbContext (CatalogContext). The second part is creating a separate ViewModel (MyOrdersViewModel ) that only contains the data we need for our Razor view. Then use this new ViewModel as a projection in our EF Query.

Side Notes

In many situations, I prefer to create a SQL Database View that I will create a new class to represent and use that as a DbSet in my DbContext. And most often I have a completely different DbContext for writes vs reads. My Reads DbContext is what will have a model that represents the SQL view. This allows you to do that projection as an SQL View rather than doing the projection in your code.

If you were using a SQL Database View and created an OrderSummary class that was on your DbContext, that now might look like this:

Live Coding Session

I did a live coding session on my YouTube and Twitch channels. You can check out the recording here of doing this exact refactor.


If you want to take a look a the full sample, you can also find my refactor changes on my fork of eShipOnWeb on GitHub.


Follow @CodeOpinion on Twitter

Enjoy this post? Subscribe!

Subscribe to our weekly Newsletter and stay tuned.