Building a Modular Monolith With Vertical Slice Architecture in .NET
Learn to build a modular monolith using vertical slice architecture in .NET. Simple words, real-life analogy, diagrams, tables, and clean C# code examples.
A school bag with proper compartments
Think about your school bag.
A bad bag is one big empty sack. You drop your books, lunch box, water bottle, pens, and gym shoes all into the same space. At first it feels fast. But soon the lunch leaks onto your notebook, your pen pokes a hole, and finding one thing means digging through everything. This is the tangled monolith — one app where all the code is thrown together.
A good bag has separate compartments. One zip pocket for books, one cool pocket for the water bottle, one closed pocket for the lunch box, and a side net for shoes. The bag is still one bag you carry to school every day. But inside, each thing has its own clean space with a wall around it. This is the modular monolith — one app you deploy together, split inside into clean modules.
Now, vertical slice architecture is how you pack each compartment. Instead of sorting by type (all caps in one shelf, all bottles in another shelf, far across the room), you pack by what you actually use together. Your full water-break kit — bottle, napkin, small towel — sits in one pocket. When you want water, you open one pocket and everything is there.
That is the whole idea of this article. One bag (modular monolith), packed by task (vertical slices). Let us build it step by step.
Two ways to cut a cake
Before code, picture a layered cake. There are two ways to cut it.
The old way cuts the cake flat, layer by layer. You get a plate of just sponge, a plate of just cream, a plate of just jam. This is the classic layered (horizontal) style: all your controllers in one folder, all your services in another, all your database code in a third. To add one new feature, you must run around and touch every layer.
The vertical slice way cuts straight down, top to bottom. Each slice has a bit of sponge, cream, and jam together. This is vertical slice architecture: each feature keeps its request, its logic, its validation, and its data access in one folder. To add a feature, you add one slice. To change it, you touch one slice.
The key insight: code that changes together should live together. A feature is a thing that changes together. So we group by feature.
The big picture: bag, then compartments
A modular monolith and vertical slices work at two different sizes.
- Modular monolith is the big picture. It splits the whole app into a few large modules, like Orders, Catalog, and Shipping. Each module has a strong wall. Modules talk to each other only through a public door (a public contract), never by reaching inside.
- Vertical slices are the small picture. Inside one module, you split the work into many tiny features, each in its own slice.
So the bag has compartments (modules), and each compartment is packed by task (slices).
Why teams pick this combo
Here is a quick comparison of the three common ways to build an app, so you can see where this combo sits.
| Style | Deploy | Inner walls | Best for |
|---|---|---|---|
| Tangled monolith | One unit | None | Tiny throwaway apps |
| Modular monolith + slices | One unit | Strong | Most teams, most apps |
| Microservices | Many units | Strong | Huge scale, many teams |
And here is why the slice part specifically helps day to day.
| Question | Layered app | Vertical slices |
|---|---|---|
| Where is the "Create Order" code? | Spread across 4 folders | All in one folder |
| What breaks if I change it? | Hard to know | Just that one slice |
| Can a new dev follow it? | Needs the whole map | Read one slice |
| How do I test it? | Mock many layers | Test one focused unit |
The folder shape
Let us make this real. Here is how the project looks on disk. Notice the modules at the top, and the feature slices inside each module.
Folder layout of a modular monolith with slices
Steps
Solution
The one app you deploy
Modules folder
Orders, Catalog, Shipping
Orders module
Owns its own database tables
Features folder
One folder per use case
CreateOrder slice
Request, handler, validator, endpoint
In text form, it looks like this:
src/
Modules/
Orders/
Features/
CreateOrder/
CreateOrder.cs // request + handler + validator
CreateOrderEndpoint.cs
GetOrder/
GetOrder.cs
OrdersDbContext.cs
Contracts/ // the public door other modules use
IOrdersApi.cs
Catalog/
Features/
...
Api/
Program.cs // wires all modules togetherEach slice folder is a small, complete story. You do not jump around to understand it.
A vertical slice in C#
Now the fun part. Let us write the Create Order slice. Everything for this one feature lives together: the request, the handler that does the work, and the validation rule. We do not use MediatR here, because MediatR is now a commercial product and needs a paid license for many teams. A plain handler class works great and stays free.
namespace Orders.Features.CreateOrder;
// The request: what the caller sends in.
public record CreateOrderRequest(Guid CustomerId, string Product, int Quantity);
// The result: what we send back.
public record CreateOrderResult(Guid OrderId);
// The handler: the actual work for this one feature.
public sealed class CreateOrderHandler
{
private readonly OrdersDbContext _db;
public CreateOrderHandler(OrdersDbContext db) => _db = db;
public async Task<CreateOrderResult> Handle(
CreateOrderRequest request,
CancellationToken ct)
{
// Simple rule lives right here in the slice.
if (request.Quantity <= 0)
throw new ArgumentException("Quantity must be at least 1.");
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = request.CustomerId,
Product = request.Product,
Quantity = request.Quantity,
CreatedAt = DateTimeOffset.UtcNow
};
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct);
return new CreateOrderResult(order.Id);
}
}Notice how the rule, the data access, and the shape of the request all sit side by side. A new teammate can read this one file and understand the whole feature. That is the win.
Now we expose it with a minimal API endpoint, again kept inside the same slice folder.
namespace Orders.Features.CreateOrder;
public static class CreateOrderEndpoint
{
public static void MapCreateOrder(this IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (
CreateOrderRequest request,
CreateOrderHandler handler,
CancellationToken ct) =>
{
var result = await handler.Handle(request, ct);
return Results.Created($"/orders/{result.OrderId}", result);
});
}
}The HTTP route and the logic stay in the same place. When you change this feature, you open one folder and you are done.
How a request flows through one slice
Here is the journey of a single web request as it passes through the slice. It is short and straight, which is exactly the point.
The most important rule: walls between modules
This is where many teams slip. The whole value of a modular monolith comes from strong walls between modules. So we follow two firm rules.
- A module never touches another module's database tables directly. Orders cannot run a SQL query against Catalog's tables. If Orders needs product info, it must ask Catalog politely.
- Modules talk only through a public contract. Each module exposes a small public interface, its "front door." Everything else inside the module is private.
Module talking to another module the right way
Steps
Orders slice
Needs a product price
ICatalogApi
The public front door
Catalog handler
Runs Catalog's own logic
Catalog DB
Private, only Catalog touches it
In code, the contract is just a small interface that lives in a shared "Contracts" spot. Other modules depend on the interface, not on the inner classes.
// Public door, shared so other modules can use it.
namespace Catalog.Contracts;
public interface ICatalogApi
{
Task<decimal?> GetPriceAsync(Guid productId, CancellationToken ct);
}
// Hidden inside Catalog: the real work.
namespace Catalog.Features.Pricing;
internal sealed class CatalogApi : ICatalogApi
{
private readonly CatalogDbContext _db;
public CatalogApi(CatalogDbContext db) => _db = db;
public async Task<decimal?> GetPriceAsync(Guid productId, CancellationToken ct)
{
var product = await _db.Products.FindAsync([productId], ct);
return product?.Price;
}
}The internal keyword is your friend here. It keeps the real class private to the Catalog module, so other modules are forced to use the public ICatalogApi door. That single keyword protects your walls.
Wiring modules together in one app
Even though modules are separate, they still live in one running app. Each module offers a small setup method that registers its own services. The main Program.cs just calls each one. This keeps the startup file clean and lets each module own its own wiring.
// In Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOrdersModule(builder.Configuration)
.AddCatalogModule(builder.Configuration)
.AddShippingModule(builder.Configuration);
var app = builder.Build();
app.MapCreateOrder(); // endpoints from the Orders slice
// ... map other module endpoints
app.Run();Each AddXModule method registers that module's handlers, its DbContext, and its public contract. The big app does not know the private details. It only knows the front doors.
How modules talk: direct calls and events
Modules can talk in two healthy ways. Pick the one that fits.
| Way | What it is | Good when |
|---|---|---|
| Direct call | Orders calls ICatalogApi and waits for the answer | You need the answer right now (like a price) |
| Domain event | Orders raises "OrderCreated", others react later | Other modules should react, but Orders should not wait |
A direct call is simple and immediate. An event keeps modules loosely tied: when an order is created, Orders just shouts "OrderCreated!" and walks away. Shipping hears it and prepares a parcel. Notifications hears it and sends an email. Orders does not know or care who is listening. This keeps the wall strong, because Orders has no link to Shipping at all.
A small note on tools. You may have heard of MediatR and MassTransit for messages and events. As of 2026, both moved to a commercial license, so they cost money for many teams. The good news: vertical slices and modular monoliths do not depend on them. You can raise and handle events with a tiny bit of your own code, or with a free library, and the pattern stays exactly the same.
When to grow into microservices
A big strength of this design is that it grows with you. Because your modules already have clean walls and talk only through public doors, you can later lift one module out into its own microservice with far less pain. You do this only when you hit a real reason, such as one module needing to scale on its own, or two teams needing to deploy separately. Until then, one app is simpler, cheaper, and faster to ship. Start as a modular monolith. Split later, if ever.
A few friendly tips
- Right-size your slices. Do not make a slice per tiny endpoint if many endpoints belong together. Group by real use case.
- Keep it simple inside a slice. A simple read can go straight to the database. You do not need extra layers when the feature is small.
- Use clean architecture only where it earns its keep. For a rich, complex slice, you can add proper layers inside that one slice. For a simple one, do not.
- Guard your walls with
internal. Make everything internal by default and open only the public contract. - Each module owns its own data. No shared tables across modules.
Quick recap
- A modular monolith is one app you deploy together, split inside into modules with strong walls, like a school bag with proper compartments.
- Vertical slice architecture packs each feature in one folder: request, handler, validation, and data access together, instead of spreading code across layers.
- The two work at different sizes: modules are the big compartments, slices are how you pack each one.
- Modules must talk only through public contracts, never by touching another module's database. The
internalkeyword helps protect this. - You can build slices without MediatR or MassTransit, which now need paid licenses. Plain handler classes work great.
- Start here for most apps. You can split a module into a microservice later only when you truly need to.
References and further reading
- Modular monoliths — Microsoft .NET architecture guidance
- Where Vertical Slices Fit Inside the Modular Monolith Architecture — Milan Jovanović
- Vertical Slice Architecture in .NET — Complete Guide (2026)
- Building a Modular Monolith With Vertical Slice Architecture in .NET — Anton Martyniuk
- On .NET Live — Clean Architecture, Vertical Slices, and Modular Monoliths (Microsoft Learn)
Related Posts
What Is a Modular Monolith? A Beginner-Friendly Guide for .NET
Understand the modular monolith in simple words: one app, strong internal walls. Learn how it compares to monoliths and microservices, why it is the 2026 default for most .NET teams, and how to build one.
Vertical Slice Architecture in .NET: The Easy Guide
Learn Vertical Slice Architecture in .NET in simple words. Organise code by feature instead of by layer, with diagrams, real examples, a comparison with Clean Architecture, and when to use each.
Where Vertical Slices Fit Inside the Modular Monolith
A simple guide to how vertical slices live inside the modules of a modular monolith in .NET, with diagrams, code, tables, and everyday examples.
How to Structure Production Apps With Vertical Slice Architecture in .NET (2026)
A simple, friendly guide to structuring real production .NET 10 apps with vertical slice architecture in 2026 — folders, code, diagrams, and tables.
Vertical Slice Architecture: The Best Ways to Structure Your Project
A simple, friendly guide to the best ways to structure a .NET 10 project with vertical slice architecture — folders, diagrams, tables, and real code.
How to Avoid Code Duplication in Vertical Slice Architecture in .NET
Learn how to avoid code duplication in Vertical Slice Architecture in .NET without breaking your slices. Rule of three, pipeline behaviors, shared infrastructure, and clear examples.