Vertical Slice Architecture in .NET showing feature-based request paths and independent use-case slices

Posted by Mahdi

Back to Blog
Software Architecture

Vertical Slice Architecture in .NET

Learn how Vertical Slice Architecture works in .NET, how to structure feature folders, which libraries to use, how to test slices, and how to avoid duplicated business logic.

Vertical Slice Architecture organises software around features, use cases, requests, commands, queries, or workflows instead of broad technical layers. In a .NET application, that means the code for creating an order, approving a quote, searching invoices, exporting a report, or updating a customer record can live together as one slice: endpoint, request model, validation, handler, persistence query, response model, mapping, tests, and local helper code.

The idea is simple: most changes are vertical. A real feature rarely touches only "controllers" or only "services" or only "repositories". It usually needs a route, input shape, validation, data access, business decision, response, and tests. Vertical Slice Architecture keeps those related changes close together, reduces accidental coupling through shared services, and lets different use cases choose the simplest design that fits their behaviour.

This article is a practical guide for .NET teams. It explains what vertical slices are, how they differ from layered architecture, which components and libraries are useful, how to structure feature folders, where EF Core and MediatR fit, how to test slices, and when the style becomes messy. VaniTech can support this work through cloud architecture, integration services, ongoing technical support, and maintainable .NET application design.

Vertical Slice Architecture in Practice

The architecture optimises for feature change: keep related code together, minimise coupling between slices, and refactor shared domain logic only when it earns its place.

Organise by Use Case

Group code around business operations such as SubmitOrder, SearchInvoices, ApproveQuote, or InviteUser.

Keep Slices Independent

A slice should avoid depending on another slice's internals. Shared code should be deliberate and small.

Use CQRS Lightly

Separate commands from queries where it helps. You do not need event sourcing or separate databases to benefit.

Choose Per Slice

One slice can use a rich domain model. Another can use a direct SQL projection. The architecture allows both.

Test Behaviour

Handler or endpoint-level tests often provide better confidence than mock-heavy tests across artificial layers.

Refactor When Needed

Repeated business rules should move into domain concepts. Repeated plumbing should move into behaviours, filters, or helpers.

Vertical Slice Architecture feature map showing endpoint request validator handler persistence response and tests grouped by use case
A vertical slice groups the code that changes together for one use case, while keeping shared framework and cross-cutting concerns around the edge.

What Is Vertical Slice Architecture?

Jimmy Bogard describes Vertical Slice Architecture as grouping all concerns for a distinct request from front end to back end, then coupling along the axis of change. Instead of creating one large Controllers folder, one Services folder, one Repositories folder, and one Validators folder, you create a feature slice that contains the pieces needed for that request.

In HTTP terms, a slice might be one endpoint: POST /orders, GET /invoices/{id}, PUT /customers/{id}/address, or POST /quotes/{id}/approve. In a worker or integration system, a slice might be one message consumer, scheduled job, import step, or workflow action.

A vertical slice is not just a folder naming convention. It is a boundary choice. The slice should contain the logic that is specific to that use case. Shared code should be pushed out only when it is genuinely shared, stable, and useful. This is the opposite of starting every feature by adding methods to global services, repository interfaces, shared DTOs, and utility classes.

Microsoft's CQRS guidance supports part of this thinking by separating write operations, or commands, from read operations, or queries. It also warns that CQRS is not suitable when the domain or business rules are simple and CRUD operations are enough. Vertical Slice Architecture can use the same command/query language without adopting a heavy CQRS implementation everywhere.

Why Layered Architecture Often Hurts Feature Work

Traditional layered architecture groups code by technical responsibility. That can be useful at first because it is familiar: API layer, application layer, domain layer, infrastructure layer, data access layer. The problem appears when most changes cut across all those layers. A developer implementing one small feature may need to edit five folders and three shared abstractions before the feature does anything useful.

This creates several practical problems:

  • Shared services become dumping grounds. A service starts as useful coordination and grows into a large class with unrelated methods.
  • Repository interfaces grow endlessly. Every new query adds another method that only one feature uses.
  • DTOs become over-shared. A change needed by one endpoint accidentally affects another endpoint.
  • Tests become mock-heavy. Tests verify interactions between layers rather than user-visible behaviour.
  • Refactoring becomes risky. A shared abstraction changes for one feature and breaks another feature that never needed it.

Vertical slices reduce these pressures by making feature code local by default. You still have shared infrastructure, logging, authentication, common primitives, and reusable domain concepts. You just stop assuming that every feature must pass through the same technical pipeline.

Component Model

What Usually Lives Inside a Slice

A .NET vertical slice can be as small as one endpoint method or as complete as a folder containing the full request path.

Endpoint

Minimal API route, Carter module, FastEndpoints endpoint, MVC action, worker consumer, or job entry point.

Request

Command, query, route parameters, body model, file upload model, or message contract for this use case.

Validator

FluentValidation rules or explicit validation for request shape, permissions, and preconditions.

Handler

The use-case body: load data, call domain logic, persist changes, publish events, and return a result.

Persistence

EF Core queries, Dapper SQL, repository calls, projections, or transaction handling that belong only to this slice.

Tests

Focused tests for the slice's behaviour, endpoint contract, validation rules, persistence, and failure cases.

A Practical .NET Folder Structure

A vertical slice solution can still have projects. The difference is that the application code inside those projects is organised by feature rather than by broad technical type.

src/
  VaniTech.Project.Api/
    Program.cs
    Features/
      Orders/
        SubmitOrder/
          Endpoint.cs
          Command.cs
          Validator.cs
          Handler.cs
          Response.cs
          Mapping.cs
          Tests.cs
        GetOrder/
          Endpoint.cs
          Query.cs
          Handler.cs
          Response.cs
      Customers/
        UpdateAddress/
        SearchCustomers/
    Shared/
      Auth/
      ProblemDetails/
      Pagination/
      Observability/
  VaniTech.Project.Domain/
    Orders/
    Customers/
    Common/
  VaniTech.Project.Infrastructure/
    Persistence/
    Messaging/
    Email/
tests/
  VaniTech.Project.Api.Tests/
  VaniTech.Project.Domain.Tests/

Some teams put each feature in one file using nested classes. Others use one folder per feature with separate files. Both can work. Use the style that helps the team navigate quickly without turning every slice into a ceremony checklist.

Recommended Slice Components

ComponentCommon .NET optionUse whenWatch out for
Endpoint routingASP.NET Core Minimal APIs, Carter, FastEndpoints, or MVC controllersYou want request handling close to the feature.Do not put major business rules directly in route lambdas.
Command/query dispatchMediatR, source-generated Mediator, or direct handler callsYou want consistent handler discovery and pipeline behaviours.Do not add a mediator only because a template did.
ValidationFluentValidation or explicit validation methodsRequest rules are meaningful and testable.Keep domain invariants in the domain, not only in validators.
PersistenceEF Core, Dapper, repositories, query services, or specificationsThe slice needs data access tailored to its command or query.Do not force all reads and writes through one generic repository.
Cross-cutting concernsMediatR behaviours, endpoint filters, middleware, decoratorsValidation, logging, timing, authorization, transactions, or idempotency repeat across slices.Keep behaviours observable and easy to debug.
ObservabilityILogger, ActivitySource, Meter, OpenTelemetryYou need traces, metrics, logs, and correlation across slices.Use low-cardinality metrics and avoid noisy domain logging.
TestingxUnit/NUnit/MSTest, WebApplicationFactory, Testcontainers, RespawnYou want confidence from behaviour and integration tests.Avoid tests that only verify mocks were called in order.

Minimal APIs, Carter, or FastEndpoints?

ASP.NET Core Minimal APIs are a natural fit because they make routes easy to define near feature code. Microsoft documents route groups as a way to organise endpoints with common prefixes, filters, authorization, and metadata. That pairs well with feature modules such as MapOrderEndpoints() or MapSubmitOrder().

Carter is a thin layer over ASP.NET Core that can make endpoint modules explicit and offers helpers such as validation extensions for Minimal APIs. It can be useful when you want feature modules without building your own convention.

FastEndpoints is more opinionated. Its documentation explicitly discusses vertical slice architecture and feature file sets. It can be helpful when you want a framework that standardises request, response, validator, endpoint, and test scaffolding for slices.

Recommendation: start with Minimal APIs if the team is comfortable with native ASP.NET Core. Use Carter if you want lightweight module organisation. Consider FastEndpoints if you want stronger conventions and scaffolding for feature slices.

Vertical slice request workflow showing endpoint validation handler persistence domain logic response and tests
Feature Flow

A Slice Should Show the Full Request Path

For a command, the flow usually moves from endpoint to validation to handler to domain or persistence to response. For a query, the flow may go straight to a projection and DTO.

MediatR and Vertical Slices

MediatR is popular in vertical slice .NET applications because each command or query can map cleanly to one handler. Its README describes it as in-process messaging with support for request/response, commands, queries, notifications, events, and generic variance. A slice might expose a Minimal API endpoint and then call sender.Send(new SubmitOrder.Command(...)).

The main benefit is not decoupling everything from everything. The benefit is a consistent use-case shape and pipeline behaviours. For example, a validation behaviour can run all FluentValidation validators for a command. A transaction behaviour can wrap selected commands. A logging behaviour can record handler timing. An idempotency behaviour can protect retryable commands.

MediatR is optional. A direct call to SubmitOrder.Handle(...) can be clearer, faster, and easier to debug in smaller systems. Newer source-generated mediator libraries can also suit teams that care about Native AOT, startup costs, or generated dispatch. The architecture works either way because vertical slices are about code locality and feature boundaries, not about one library.

Validation in a Slice

FluentValidation works well when request validation is more than a couple of null checks. Its ASP.NET Core documentation recommends manual validation as the most straightforward approach and warns that the older ASP.NET validation pipeline approach is not recommended for new projects because it is synchronous, MVC-only, and harder to debug. That matters for vertical slices because many .NET teams now use Minimal APIs or endpoint modules.

Use validators for request-level rules:

  • required fields;
  • string lengths and formats;
  • numeric ranges;
  • cross-field checks;
  • basic permission or state preconditions when they are cheap to check;
  • asynchronous existence checks when appropriate.

Do not treat validators as the only protection for the domain. A Money value object should still reject invalid currency or negative amounts when the business requires it. An Order aggregate should still prevent invalid state transitions. Validators protect the use-case boundary; domain objects protect business truth.

EF Core in a Vertical Slice

EF Core can be used directly inside a slice handler, especially for simple reads or commands where the handler is already the application boundary. Microsoft's EF Core documentation says a DbContext is designed for a short unit-of-work lifetime and is not thread-safe. In ASP.NET Core, the usual scoped context per request often maps naturally to a single command or query slice.

The important design choice is not "repository or no repository". It is whether the persistence code is clear, testable, and local to the use case. A query slice may use EF Core projection directly:

var invoice = await db.Invoices
    .Where(x => x.Id == request.InvoiceId)
    .Select(x => new Response(x.Id, x.Number, x.Total, x.Status))
    .SingleOrDefaultAsync(cancellationToken);

A command slice with richer domain behaviour may load an aggregate through a repository or query method, call domain methods, and save changes. A reporting slice might use Dapper or raw SQL because the result is a read model. Vertical Slice Architecture allows the persistence style to match the use case instead of forcing every request through the same abstraction.

CQRS Without Overbuilding

Vertical slices often feel like CQRS because reads and writes naturally differ. Microsoft describes CQRS as separating commands that update data from queries that retrieve data. It also notes benefits such as separate models, simpler queries, security separation, and independent optimisation, while warning against using it when simple CRUD is enough.

For most business applications, start with lightweight CQRS:

  • name commands after business tasks, such as ApproveQuote or CancelBooking;
  • name queries after user questions, such as GetQuoteSummary or SearchBookings;
  • let commands protect business rules and consistency;
  • let queries return DTOs or projections optimised for the screen;
  • avoid separate databases unless scale, security, or integration requirements justify them.

This gives the team most of the clarity without adding event sourcing, distributed transactions, or complex synchronisation before the business needs them.

Domain Logic: Keep It Local Until It Repeats

A common criticism of vertical slices is that they can duplicate logic. That criticism is fair when a team copies business rules across handlers and never refactors. Bogard explicitly warns that the style assumes the team understands code smells and refactoring. Without that discipline, feature folders become organised copy-paste.

The practical rule is: duplicate plumbing briefly, but do not duplicate business truth. If two slices format a response slightly differently, that may be fine. If two slices calculate eligibility, pricing, entitlement, tax, limits, or status transitions, that rule probably deserves a domain concept.

Refactoring paths include:

  • move repeated validation into a reusable validator or helper;
  • move repeated business rules into a value object, domain service, policy, or aggregate method;
  • move repeated query criteria into a specification or query object;
  • move repeated handler plumbing into a pipeline behaviour or endpoint filter;
  • move repeated external calls into an adapter or gateway interface;
  • move repeated response shaping into a mapper only when it remains mechanical.

Cross-Cutting Concerns

Vertical slices do not mean every slice reimplements logging, validation, transactions, authorization, or exception handling. Cross-cutting concerns should still be centralised when they are genuinely cross-cutting.

Good .NET options include:

  • Middleware: request logging, exception handling, correlation IDs, security headers, and response compression.
  • Endpoint filters: Minimal API validation, authorization checks, tenant loading, and per-group policies.
  • MediatR pipeline behaviours: validation, performance timing, transactions, idempotency, caching, and command authorization.
  • Decorators: caching, retry, timing, and instrumentation around interfaces.
  • Infrastructure adapters: email, payment, search, storage, and messaging clients.

The trick is to keep cross-cutting code boring and visible. If a pipeline behaviour silently changes data, swallows exceptions, or performs hidden business decisions, it becomes another kind of global service problem.

Observability by Slice

Vertical slices give operations teams a useful measurement unit: the use case. Instead of only tracking controller names or database calls, you can track SubmitOrder, ApproveQuote, SearchInvoices, or ImportProducts. Microsoft describes observability in .NET through logs, metrics, and distributed tracing, and notes that .NET exposes APIs such as ILogger, Meter, and ActivitySource that OpenTelemetry can collect and export.

Useful telemetry per slice includes:

  • request count and failure count;
  • handler duration and dependency duration;
  • validation failure categories;
  • authorization failure categories;
  • database query count and slow queries;
  • external API errors;
  • retry and idempotency outcomes;
  • domain event or integration event publish outcomes.

Use tags carefully. Feature names and result categories are usually fine. User IDs, free-text search terms, emails, and high-cardinality values can create privacy and cost problems.

Testing Vertical Slices

Vertical Slice Architecture tends to favour behaviour tests over mock-heavy layer tests. A good test might send a command to a handler with a real in-memory or containerised database, assert the returned response, and check the persisted state. Another test might call an endpoint through WebApplicationFactory and assert the HTTP status, validation response, authorization behaviour, and database outcome.

Test at several levels:

  • Domain unit tests: fast tests for value objects, aggregates, policies, and important business rules.
  • Validator tests: focused checks for request rules and error messages.
  • Handler tests: use-case tests that exercise validation, persistence, and domain logic.
  • Endpoint tests: HTTP contract tests for routing, auth, request binding, response shape, and problem details.
  • Integration tests: real database, real migrations, realistic transactions, and external dependencies replaced with fakes or containers.

Respawn can help reset test databases to a clean state by deleting table data in dependency order. Testcontainers can help run real infrastructure in integration tests. You do not need both on day one, but the testing strategy should prove behaviour through the slice rather than only proving that one mocked layer called another mocked layer.

When Vertical Slice Architecture Works Best

Vertical slices are a strong fit when:

  • the application has many distinct commands, queries, workflows, or endpoints;
  • features change independently;
  • read and write paths need different models;
  • the current service layer has become too large;
  • shared DTOs and repositories cause side effects;
  • the team wants more local reasoning when changing features;
  • integration or endpoint-level tests give useful confidence;
  • the team is comfortable refactoring repeated business concepts into domain code.

This makes the style useful for SaaS applications, admin portals, customer portals, workflow systems, ecommerce back offices, integration APIs, booking systems, case management, internal tools, and .NET APIs with many business operations.

When It Is Not a Good Fit

Vertical Slice Architecture can be the wrong move when the team wants a folder structure to solve a modelling problem. It will not fix unclear business rules, weak domain understanding, poor testing, or lack of ownership. It can also be excessive for a very small CRUD app where a simple page model or controller is enough.

Be cautious when:

  • the team copies code but rarely refactors;
  • each slice invents a different style without shared conventions;
  • business rules are duplicated across handlers;
  • developers cannot find common behaviour because feature folders are too deep;
  • cross-cutting behaviours are hidden in too many filters and pipelines;
  • the team uses MediatR, CQRS, and feature folders mechanically without understanding why.

A good vertical-slice codebase still needs conventions. It just applies them around use cases rather than artificial layers.

A 30-Day Adoption Plan

  1. Choose one new feature. Do not reorganise the whole codebase first. Pick a real command or query that is meaningful but not mission-critical.
  2. Create a feature folder. Include endpoint, request, validator, handler, response, persistence, and tests only as needed.
  3. Keep the first slice boring. Use Minimal APIs or your existing web framework. Avoid adding a mediator, mapper, specification library, and new test stack all at once.
  4. Add validation explicitly. Use FluentValidation or clear hand-written validation where it improves readability.
  5. Let queries project directly. Avoid forcing read models through aggregate repositories unless the read truly needs domain behaviour.
  6. Protect commands with domain rules. Move important invariants into domain concepts when they emerge.
  7. Add one cross-cutting mechanism at a time. Start with validation or logging. Add transaction behaviours and idempotency only where the use case needs them.
  8. Write behaviour tests. Prefer tests that prove the slice works through the handler or endpoint.
  9. Review duplication after three slices. Refactor repeated business rules, not every repeated line.
  10. Document conventions. Agree naming, folder depth, validation style, persistence style, and testing expectations.

Final Recommendation

Vertical Slice Architecture is best understood as a feature-change architecture. It asks a practical question: when this use case changes, where should the related code live so the change is easy to make, test, and review? In .NET, the answer is often a feature folder containing the endpoint, request, validator, handler, persistence path, response, and tests.

Use Minimal APIs, Carter, FastEndpoints, MediatR, FluentValidation, EF Core, OpenTelemetry, and testing tools when they make slices clearer. Skip them when they only add ceremony. Keep slices independent, keep domain truth out of copy-paste handlers, and use cross-cutting behaviours carefully. The result should be a codebase where adding a feature mostly adds code in one place rather than modifying a chain of shared layers.

Sources Checked

FAQs

Vertical Slice Architecture FAQs

Short answers for .NET teams deciding whether to organise applications by features and use cases.

Next Step

Make Feature Delivery Easier to Change

VaniTech can help assess your .NET architecture, reduce service-layer complexity, design vertical slices, improve testing, and create a practical migration path from layered code.