Skip to main content
SEMastery
Architectureintermediate

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.

13 min readUpdated May 8, 2026

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.

Two questions at two sizes: modules split the system, slices organise a feature.

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.
A modular monolith: one app, several modules, each owning its own data.

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

Request
Validate
Handle
Data
Response

Steps

1

Request

endpoint receives input

2

Validate

check the input

3

Handle

run the business logic

4

Data

read or write the database

5

Response

send the result back

A single feature, complete from request to response.

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.

Slices nest inside modules, which nest inside the application.

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

Orders slice
Catalog public API
Catalog data

Steps

1

Orders slice

needs a product price

2

Catalog public API

GetPriceAsync is called

3

Catalog data

only Catalog reads its own tables

Through a public API, never through the database.

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.

Direct call for an answer now, event for a reaction later.

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.

QuestionModular monolithVertical slice
What size is it?Macro: the whole systemMicro: one feature
What does it own?A business area and its dataOne use case end to end
What is the unit?A module like OrdersA slice like Create Order
Who can it touch?Only public APIs of other modulesOnly code inside its own module
AnalogyA shop in the mallOne sales routine in the shop

And here is how the two work together in everyday tasks.

TaskThe module decidesThe slice decides
Add a "Cancel Order" featureIt belongs to OrdersA new CancelOrder slice
Read a product priceAsk Catalog's public APIThe Create Order slice makes the call
Store order dataOrders owns its own schemaThe slice writes through OrdersDbContext
Notify shippingPublish an event across modulesThe 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

HTTP request
Orders module
Create Order slice
Catalog API
Save and respond

Steps

1

HTTP request

POST /orders arrives

2

Orders module

request enters the right module

3

Create Order slice

validate and handle

4

Catalog API

fetch the price properly

5

Save and respond

write to Orders data, return 201

The wall is the module, the room is the slice.

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 handlers internal. 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.

Strong walls plus tidy rooms give you a system that is easy to change.

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

Related Posts