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.
A shopping mall with many shops
Picture a big shopping mall in your city.
The mall is one large building with one main entrance, one car park, and one electricity connection. From outside, it looks like a single thing.
Inside, the mall is divided into separate shops. There is a clothes shop, a sweet shop, a mobile shop, and a bookshop. Each shop has its own counter, its own stock room, and its own staff. The sweet shop staff do not walk into the mobile shop's stock room and grab phones. If the sweet shop needs phones for a lucky draw, it asks the mobile shop at its counter.
Now look inside one shop. The clothes shop sells shirts, trousers, and shoes. For each kind of sale, the staff have a clear routine: greet the customer, pick the item, check the price, take payment, hand over the bill. That one complete routine, from greeting to bill, is a vertical slice.
So we have three sizes of thing:
- The mall is the whole application.
- Each shop is a module.
- Each complete sales routine inside a shop is a vertical slice.
A modular monolith is the mall. Vertical slices are the routines inside each shop. This article is about exactly where those slices live and how they behave.
The two questions, at two sizes
People often argue about "modular monolith versus vertical slices" as if you must pick one. That is the wrong way to look at it. They answer different questions, at different sizes.
The modular monolith works at the macro level. It decides how the whole system breaks into modules and how those modules are allowed to talk. It draws the walls.
The vertical slice works at the micro level. It decides how the code for one single feature is arranged inside a module. It furnishes one room.
Because they sit at different sizes, they fit together neatly. The modular monolith gives you strong outer walls. Vertical slices give you tidy rooms inside those walls. You are not choosing between them. You are using both, at the same time, for different jobs.
A quick refresher on the modular monolith
A modular monolith is a single application that you build and deploy as one unit, but inside it is split into clear modules.
Each module owns one business area. In an online shop you might have a Catalog module, an Orders module, a Payments module, and a Shipping module. Each module has strong boundaries:
- A module owns its own data. No other module reads its tables directly.
- A module exposes a small public API. Everything else inside it is
internal. - Modules talk to each other only through that public API or through events.
The strong walls are the whole point. They keep the system from turning into a "big ball of mud" where everything depends on everything. If one day you want to pull the Payments module out into its own microservice, the wall is already there, so the move is much easier.
A quick refresher on vertical slices
Vertical Slice Architecture organises code by feature, not by technical layer.
In an old layered app, the code for one feature is scattered. A "Create Order" feature has a controller in one folder, a service in another, and a repository in a third. To change the feature you hop across many folders.
A vertical slice keeps everything for one feature together: the endpoint, the logic, the validation, and the data access live side by side, in one place.
What lives in one slice
Steps
Request
endpoint receives input
Validate
check the input
Handle
run the business logic
Data
read or write the database
Response
send the result back
The big win is that change stays small. When a feature changes, you usually touch one slice and nothing else. You do not ripple edits across five layer folders.
Putting them together: slices live inside modules
Now the key idea of this whole article.
A vertical slice lives inside one module. It does not stretch across modules. The module is the wall. The slice is a room inside that wall.
So the full picture looks like this. The application holds modules. Each module holds a set of vertical slices. Each slice is one complete feature for that module.
Notice that every slice sits clearly inside one box. The "Create Order" slice belongs to Orders. The "Search Products" slice belongs to Catalog. No slice has one foot in two modules.
This gives us a simple rule you can remember: a slice never crosses a module wall. If a slice needs help from another module, it knocks on that module's public door. It never climbs through the window into another module's data.
What a slice looks like in code
Here is a tiny "Create Order" slice inside the Orders module. Everything for the feature sits in one file: the request, the endpoint, and the handler. This is plain .NET 10 with C# 14, no mediator library needed.
namespace Shop.Modules.Orders.Features.CreateOrder;
// The request the customer sends
public sealed record CreateOrderRequest(Guid CustomerId, List<OrderLine> Lines);
public sealed record OrderLine(Guid ProductId, int Quantity);
// The endpoint maps the route to the handler
public static class CreateOrderEndpoint
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (
CreateOrderRequest request,
CreateOrderHandler handler,
CancellationToken ct) =>
{
var orderId = await handler.Handle(request, ct);
return Results.Created($"/orders/{orderId}", new { id = orderId });
});
}
}The handler holds the logic and the data access for this one feature. It stays internal, because nothing outside the Orders module should call it directly.
namespace Shop.Modules.Orders.Features.CreateOrder;
internal sealed class CreateOrderHandler(OrdersDbContext db)
{
public async Task<Guid> Handle(CreateOrderRequest request, CancellationToken ct)
{
var order = Order.Create(request.CustomerId, request.Lines);
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return order.Id;
}
}Look at what is missing. There is no call into the Payments tables. There is no SELECT against the Catalog database. This slice stays fully inside the Orders module. If it needs a price from Catalog, it must ask Catalog properly, which we cover next.
When a slice needs another module
Sometimes a feature truly needs data from another module. The Orders module may need the current price of a product, which lives in Catalog.
The wrong way is to let the Create Order slice read the Catalog tables. That breaks the wall and the modules become tangled again.
The right way is to ask Catalog through its public API. Catalog exposes a small, clear contract. Orders depends on that contract, not on Catalog's insides.
// Public contract that the Catalog module exposes.
// Other modules are allowed to depend on this.
namespace Shop.Modules.Catalog.PublicApi;
public interface ICatalogModuleApi
{
Task<ProductPrice?> GetPriceAsync(Guid productId, CancellationToken ct);
}
public sealed record ProductPrice(Guid ProductId, decimal Amount, string Currency);Inside the Orders slice, you take that interface and call it. Orders never touches a Catalog table directly.
Orders asks Catalog the right way
Steps
Orders slice
needs a product price
Catalog public API
GetPriceAsync is called
Catalog data
only Catalog reads its own tables
For things that can happen later, like telling Shipping that an order was placed, you can use an event instead of a direct call. Orders publishes an "OrderPlaced" event. Shipping listens and reacts. The two modules stay loosely joined.
How the folders are arranged
The folder layout makes the nesting clear. Modules at the top, features inside each module, files inside each feature.
Shop/
Modules/
Orders/
Features/
CreateOrder/ <- one vertical slice
CreateOrderEndpoint.cs
CreateOrderHandler.cs
CancelOrder/ <- another slice
GetOrder/ <- another slice
Domain/
OrdersDbContext.cs
PublicApi/
Catalog/
Features/
AddProduct/
SearchProducts/
PublicApi/You can read this layout like a sentence. "Inside the Shop app, inside the Orders module, inside the Features folder, the Create Order slice has its endpoint and its handler." Each level of nesting matches a level in our mall analogy: mall, shop, sales routine.
Module versus slice: a side by side
It helps to see the two ideas in a table so the sizes stay clear in your head.
| Question | Modular monolith | Vertical slice |
|---|---|---|
| What size is it? | Macro: the whole system | Micro: one feature |
| What does it own? | A business area and its data | One use case end to end |
| What is the unit? | A module like Orders | A slice like Create Order |
| Who can it touch? | Only public APIs of other modules | Only code inside its own module |
| Analogy | A shop in the mall | One sales routine in the shop |
And here is how the two work together in everyday tasks.
| Task | The module decides | The slice decides |
|---|---|---|
| Add a "Cancel Order" feature | It belongs to Orders | A new CancelOrder slice |
| Read a product price | Ask Catalog's public API | The Create Order slice makes the call |
| Store order data | Orders owns its own schema | The slice writes through OrdersDbContext |
| Notify shipping | Publish an event across modules | The slice raises OrderPlaced |
What a request looks like end to end
Let us follow one request through the whole structure, so the layers feel real. A customer places an order.
One request through the modular monolith
Steps
HTTP request
POST /orders arrives
Orders module
request enters the right module
Create Order slice
validate and handle
Catalog API
fetch the price properly
Save and respond
write to Orders data, return 201
The request lands on the application. The router sends it into the Orders module. Inside Orders, the Create Order slice takes over. The slice validates the input, asks Catalog for the price through the public API, builds the order, and saves it to the Orders schema. Then it returns a 201 Created result with the new id, for example pointing at GET /orders/{id}.
At no point does the slice peek into another module's tables. The wall held. The room did its job.
Common mistakes to avoid
A few traps catch beginners. Knowing them early saves a lot of pain.
- Slices that cross modules. If one slice reads Catalog data and writes Orders data, you have torn the wall. Split it: keep the Orders work in an Orders slice and ask Catalog through its API.
- Public by default. If every class is
public, any module can call any other. Keep slice handlersinternal. Make only the small public API visible. - Sharing a DbContext across modules. One shared context that sees all tables quietly destroys the walls. Give each module its own context and schema.
- Forcing one inner style everywhere. A module is free to use slices, Clean Architecture, or almost nothing. Pick what fits that module's complexity, not a rule for the whole app.
- Reaching for heavy tools too soon. You do not need MediatR or MassTransit to start. Both are now commercially licensed. Plain handler classes and a simple in-process event bus are enough for a first modular monolith.
Why this combination works so well
The pairing is popular for a simple reason: the two ideas cover each other's weak spots.
A modular monolith on its own tells you where the walls go, but not how to arrange code inside a module. Without guidance, a module can still grow messy inside.
Vertical slices on their own keep features tidy, but they do not stop one feature from reaching into unrelated parts of the system. Without walls, slices can sprawl.
Put together, the module gives the slice a safe home with clear edges, and the slice keeps that home neat one feature at a time. You get strong boundaries and small, easy changes.
There is a bonus. Because each module already has a clear wall and a public API, pulling a module out into its own microservice later is far less painful. The seam is drawn before you ever need it. Your modular monolith with vertical slices is, in a quiet way, also a head start on microservices, if you ever need them.
Quick recap
- A modular monolith is the whole app split into modules with strong walls. It is the macro view.
- A vertical slice is one feature arranged end to end. It is the micro view.
- Think mall, shop, sales routine: app, module, slice.
- A slice lives inside one module and never crosses a module wall.
- When a slice needs another module, it asks through that module's public API or an event, never by reading its tables.
- Each module can choose its own inner style: slices, Clean Architecture, or something simple.
- Keep handlers
internal, give each module its own data, and you do not need commercially licensed tools to begin. - Together, modules give strong boundaries and slices give small, safe changes.
References and further reading
- Where Vertical Slices Fit Inside the Modular Monolith — Milan Jovanović
- On .NET Live - Clean Architecture, Vertical Slices, and Modular Monoliths — Microsoft Learn
- Vertical Slice Architecture in .NET - Complete Guide — Milan Jovanović
- Building a Modular Monolith With Vertical Slice Architecture in .NET — Anton Martyniuk
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.
Internal vs Public APIs in Modular Monoliths (.NET Guide)
Learn the difference between internal and public APIs in a .NET modular monolith, why module boundaries matter, and how to expose only safe contracts to other modules.
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.
Monolith to Microservices: How a Modular Monolith Helps
Learn how a modular monolith makes the move from monolith to microservices safe and easy in .NET, using clean boundaries, the Strangler Fig pattern, and small steps.
Breaking It Down: How to Migrate Your Modular Monolith to Microservices
A friendly, step-by-step guide to safely move from a .NET modular monolith to microservices using the Strangler Fig pattern, without a risky big-bang rewrite.