Skip to main content

Vertical Slice Architecture

Vertical slice architecture organizes code by feature instead of by technical layer. Each feature (or "slice") contains everything it needs -- the request DTO, the message, the handler, and the endpoint registration -- in one place.

Why Vertical Slices?

Traditional layered architecture groups code by technical concern:

Controllers/
PatientsController.cs
AppointmentsController.cs
Services/
PatientService.cs
AppointmentService.cs
Repositories/
PatientRepository.cs
AppointmentRepository.cs
Models/
Patient.cs
Appointment.cs

Adding a new feature means touching multiple folders across the project. Vertical slices flip this: each folder is a self-contained feature.

Layered vs Vertical Slice

AspectLayeredVertical Slice
Organize byTechnical concern (Controllers, Services, Repos)Feature (Patients, Appointments)
Adding a featureTouch 3-5 foldersAdd 1 folder
CouplingLayers couple to each other horizontallySlices are independent
Shared codeEverything is shared (by layer)Sharing is explicit and intentional
NavigationJump between folders to understand a featureOne folder tells the whole story
Delete a featureHunt across layersDelete one folder

Screaming Architecture

A screaming architecture makes the domain obvious from the folder structure. When you open the project, the folder names should tell you what the system does, not what framework it uses.

// Screaming: folders describe the domain
Features/
Patients/
Appointments/
Prescriptions/
Notifications/

// Not screaming: folders describe the framework
Controllers/
Services/
Repositories/
Models/

Why Dispatch Maps Naturally to Slices

Dispatch's message-handler model is a natural fit for vertical slices:

  • One message = one operation (RegisterPatientCommand, GetPatientQuery)
  • One handler = one behavior (RegisterPatientHandler)
  • Events enable cross-slice communication without direct coupling
  • No service layer needed -- the handler is the behavior

Each slice becomes: Request DTO + Message + Handler + Endpoint registration.

Folder Structure

Features/
Patients/ <-- Feature slice
RegisterPatient/ <-- One folder per operation
RegisterPatientRequest.cs <-- API request DTO
RegisterPatientCommand.cs <-- IDispatchAction<TResult> message
RegisterPatientHandler.cs <-- IActionHandler implementation
GetPatient/
GetPatientRequest.cs
GetPatientQuery.cs
GetPatientHandler.cs
UpdatePatientInfo/
UpdatePatientInfoRequest.cs
UpdatePatientInfoCommand.cs
UpdatePatientInfoHandler.cs
Events/ <-- Domain events published by this slice
PatientRegistered.cs
Shared/ <-- DTOs, repositories scoped to the slice
PatientDto.cs
IPatientRepository.cs
InMemoryPatientRepository.cs
PatientsEndpoints.cs <-- Maps HTTP routes to Dispatch messages
PatientsServiceCollectionExtensions.cs <-- Per-slice DI registration

Appointments/ <-- Another independent slice
Prescriptions/ <-- Shows [Authorize] on message types
Notifications/ <-- Event-only slice (no HTTP endpoints)

Naming Conventions

ItemPatternExample
Operation folder{Verb}{Noun}RegisterPatient/
Request DTO{Operation}RequestRegisterPatientRequest.cs
Command message{Operation}CommandRegisterPatientCommand.cs
Query message{Operation}QueryGetPatientQuery.cs
Handler{Operation}HandlerRegisterPatientHandler.cs
Domain event{PastTense}PatientRegistered.cs
Endpoints file{Feature}EndpointsPatientsEndpoints.cs
DI extensions{Feature}ServiceCollectionExtensionsPatientsServiceCollectionExtensions.cs

Per-Slice DI Registration

Each slice registers its own services using an extension method in the Microsoft.Extensions.DependencyInjection namespace:

using HealthcareApi.Features.Patients.Shared;

namespace Microsoft.Extensions.DependencyInjection;

public static class PatientsServiceCollectionExtensions
{
public static IServiceCollection AddPatientsFeature(this IServiceCollection services)
{
services.AddSingleton<IPatientRepository, InMemoryPatientRepository>();
return services;
}
}

The composition root calls each feature's registration method:

builder.Services.AddPatientsFeature();
builder.Services.AddAppointmentsFeature();
builder.Services.AddPrescriptionsFeature();
builder.Services.AddNotificationsFeature();

Cross-Slice Communication

Slices communicate through domain events, never by direct reference to another slice's handlers or repositories.

Publishing Events

A handler in the Patients slice publishes PatientRegistered:

public sealed class RegisterPatientHandler
: IActionHandler<RegisterPatientCommand, RegisterPatientResult>
{
private readonly IPatientRepository _patients;
private readonly IDispatcher _dispatcher;

public async Task<RegisterPatientResult> HandleAsync(
RegisterPatientCommand action, CancellationToken cancellationToken)
{
var id = Guid.NewGuid();
// ... save patient ...

await _dispatcher.DispatchAsync(
new PatientRegistered(id, action.Email), cancellationToken)
.ConfigureAwait(false);

return new RegisterPatientResult { PatientId = id };
}
}

Handling Events in Another Slice

The Notifications slice subscribes to events from other slices without any coupling:

namespace HealthcareApi.Features.Notifications.Handlers;

public sealed class PatientRegisteredNotificationHandler
: IEventHandler<PatientRegistered>
{
private readonly INotificationService _notifications;

public async Task HandleAsync(
PatientRegistered eventMessage, CancellationToken cancellationToken)
{
await _notifications.SendAsync(
$"patient-{eventMessage.PatientId}",
"Welcome",
"Your account has been created.",
cancellationToken).ConfigureAwait(false);
}
}

The only shared artifact is the event record itself (PatientRegistered), which lives in the publishing slice's Events/ folder.

Per-Slice Authorization

Use [Authorize] attributes directly on message types. The ASP.NET Core Authorization Bridge evaluates them in the Dispatch pipeline:

[Authorize(Roles = "Physician")]
public record CreatePrescriptionCommand(Guid PatientId, string Medication, string Dosage)
: IDispatchAction<CreatePrescriptionResult>;

No need for policy configuration in the endpoint registration -- the attribute on the message type handles it.

Endpoint Registration

Each slice registers its own routes via an extension method on IEndpointRouteBuilder:

public static class PatientsEndpoints
{
public static RouteGroupBuilder MapPatientsEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/patients").WithTags("Patients");

group.DispatchPostAction<RegisterPatientRequest, RegisterPatientCommand, RegisterPatientResult>(
"/", static (request, _) => new RegisterPatientCommand(
request.FirstName, request.LastName, request.DateOfBirth, request.Email));

group.DispatchGetAction<GetPatientRequest, GetPatientQuery, PatientDto>(
"/{id:guid}", static (request, _) => new GetPatientQuery(request.Id));

group.DispatchPutAction<UpdatePatientInfoRequest, UpdatePatientInfoCommand>(
"/{id:guid}", static (request, httpContext) => new UpdatePatientInfoCommand(
Guid.Parse(httpContext.GetRouteValue("id")!.ToString()!),
request.Email, request.Phone));

return group;
}
}

The composition root wires everything together:

var api = app.MapGroup("/api");
api.MapPatientsEndpoints();
api.MapAppointmentsEndpoints();
api.MapPrescriptionsEndpoints();

See Minimal API Hosting Bridge for the full endpoint routing API reference.

When to Use Vertical Slices

SituationRecommendation
Feature-rich API with many independent operationsVertical slices
CRUD app with uniform data access patternsLayered may be simpler
Team organized by feature ownershipVertical slices align with team structure
Shared business logic across many featuresExtract to a shared library, keep slices for orchestration
Microservices with focused responsibilitiesEach service is already a "slice" -- use either approach internally

Vertical slices work well when features are independent and the team wants to minimize cross-cutting changes. They pair naturally with Dispatch's message-per-operation model.

See Also