Vertical Slice Architecture: Where Does the Shared Logic Live?
A beginner-friendly guide to where shared logic lives in Vertical Slice Architecture in .NET — domain, infrastructure, pipelines, and the rule of three.
A shared kitchen in a hostel
Picture a student hostel with many rooms. Each room is like a little home. The students cook their own special dishes in their own rooms. One room loves spicy biryani. Another room makes simple khichdi. Each room owns its own recipe and its own taste.
But the hostel still has one shared kitchen downstairs. That kitchen has the big gas stove, the water tap, the fridge, and the dustbin. Nobody puts their secret recipe in the shared kitchen. They only keep the common tools there, like the stove and the tap, because every room needs those.
This is exactly how Vertical Slice Architecture (VSA) works. Each feature is like a room with its own recipe. The shared kitchen holds only the common tools that everyone needs. The big question this article answers is simple: which things go in the room, and which things go in the shared kitchen?
What a slice actually is
In VSA, you organise your code by feature, not by technical layer. A slice is one full feature, top to bottom. It holds the request, the validation, the business rule, and the database call, all in one place.
Compare this with the old layered way. In the layered way, you have a Controllers folder, a Services folder, and a Repositories folder. To add one feature, you touch all three folders. The code for one feature is scattered.
In VSA, the code for one feature sits together in one folder. You change a feature by opening one place.
When each feature lives on its own, a natural worry appears. If features do not share folders, do they end up copying code everywhere? And when something really is shared, where should it go? Let us answer that step by step.
Three kinds of "shared"
The word "shared" is too broad. It hides three very different things. Before deciding where shared code lives, you must know which kind of sharing you are looking at.
| Kind of shared code | Example | Changes per feature? | Where it lives |
|---|---|---|---|
| Technical plumbing | Logging, database context, caching | Almost never | Infrastructure or Shared |
| Cross-cutting behavior | Validation, auth checks, timing | Rarely | Pipeline behaviors or middleware |
| Business logic | Pricing rules, order calculations | Often | Domain, and only when truly shared |
The trick is to treat each kind differently. You share technical plumbing freely. You share cross-cutting behavior through pipelines. You share business logic very carefully, and only when it is genuinely the same.
Deciding where shared code belongs
Steps
Is it technical?
Yes: put it in Infrastructure/Shared
Is it cross-cutting?
Yes: put it in a pipeline behavior
Is it the same rule 3x?
Yes: move it to Domain
Keep it in the slice
No to all: leave it where it is
Technical plumbing goes in the shared kitchen
Some code has nothing to do with any one feature. The database connection. The logger. The HTTP client that calls a payment provider. The clock that gives the current time.
This is the stove and the tap of your app. Every slice needs it, and it rarely changes when a feature changes. So you set it up once and let every slice use it.
In a typical .NET app, this plumbing is registered once at startup.
// Program.cs — shared plumbing set up once for the whole app
var builder = WebApplication.CreateBuilder(args);
// Database context: shared by every slice
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
// A shared clock so slices do not call DateTime.Now directly
builder.Services.AddSingleton<IClock, SystemClock>();
// A shared HTTP client for a payment provider
builder.Services.AddHttpClient<IPaymentGateway, StripePaymentGateway>();
var app = builder.Build();Notice what is shared here. It is the wiring, not the rules. Sharing a database context does not couple your features together, because two slices can both read from the same database while keeping totally different logic. The stove is shared. The recipe is not.
Cross-cutting concerns go in pipeline behaviors
Some things must happen on every request. You want to log every call. You want to validate the input. You want to check that the user is allowed in. You do not want to copy that code into every single handler. That would be slow to write and easy to get wrong.
The clean answer is a pipeline. A pipeline wraps around your handler. It runs before and after the real work, like a security guard at a gate. Every request passes through the same guard, but the guard does not know or care what each room cooks.
Here is a small validation behavior. It runs for every request, so each slice stays free of repeated validation plumbing.
// A shared pipeline behavior that validates any request before the handler runs.
public sealed class ValidationBehavior<TRequest, TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
=> _validators = validators;
public async Task<TResponse> Handle(
TRequest request,
Func<Task<TResponse>> next,
CancellationToken ct)
{
foreach (var validator in _validators)
{
var result = await validator.ValidateAsync(request, ct);
if (!result.IsValid)
throw new ValidationException(result.Errors);
}
// All checks passed, so let the real slice handler run.
return await next();
}
}A quick note on tooling. Many tutorials use a library called MediatR to build these pipelines, and MassTransit for messaging. As of late 2024 both moved to a commercial license for larger companies. They still work well, but they are no longer free for every business. You do not have to use them. You can build a simple pipeline yourself with a small interface, or use a free community alternative. The idea of a pipeline matters far more than the brand of library.
Business logic is the careful part
This is where most mistakes happen. A new student sees two slices that look alike and thinks, "I will pull this into a shared service to be DRY." Then six months later, the two features need to change in different ways, and the shared service becomes a knot that hurts both.
The safe rule is the rule of three. Wait until three slices truly do the same thing before you extract it. One copy is normal. Two copies might be a coincidence. Three copies is a real pattern.
The rule of three for extracting business logic
Steps
1 copy
Normal: leave it in the slice
2 copies
Maybe a coincidence: wait
3 copies
A real pattern has appeared
Extract to Domain
Now move it and reuse it
When business logic really is shared, it belongs in a Domain folder. The Domain holds entities, value objects, and a few domain services. A Customer entity, an Order entity, a Money value object that knows how to add amounts safely. These are shared because they are the true meaning of your business, not just a convenience.
// A value object in the Domain folder, shared by many slices.
// It protects a real business rule: you cannot add two different currencies.
public readonly record struct Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException(
"Cannot add money in different currencies.");
return this with { Amount = Amount + other.Amount };
}
}This is safe to share because the rule "you cannot add rupees to dollars" is true for every feature. It will not change just because one feature changes.
The folder structure that ties it together
Here is a folder layout that puts each kind of code in its right home. Notice the small Shared folder inside a feature. That is for logic shared by only the slices of that one feature, like a small helper used by both CreateOrder and UpdateOrder.
| Folder | What it holds | Who uses it |
|---|---|---|
Features/Orders/CreateOrder | One full slice | Only this slice |
Features/Orders/Shared | Helpers for Order slices | Only Order slices |
Domain | Entities, value objects, rules | Many features |
Infrastructure | Database, clients, logging | The whole app |
Common | Pipeline behaviors, Result type | The whole app |
Most "sharing" is just reading data
Here is a surprise that trips up many people. A lot of what looks like shared logic is really just reading data from the same database.
Say CreateOrder needs the customer's address. The wrong move is to call into a "Customers feature service." That ties two features together with a hidden rope. The right move is simple: the CreateOrder slice reads the customer row from the database itself. It owns its own data access.
Each slice talks to the shared database directly. It does not talk to other slices. The database is the shared kitchen tap. Two rooms can both fill water from the same tap without becoming the same room.
// The CreateOrder slice reads the data it needs directly.
// It does NOT call into a "Customers" feature service.
public async Task<OrderId> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
// Read shared data straight from the database.
var customer = await _db.Customers
.FirstOrDefaultAsync(c => c.Id == cmd.CustomerId, ct)
?? throw new NotFoundException("Customer not found.");
var order = Order.Create(customer.Id, cmd.Items);
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct);
return order.Id;
}This keeps coupling low. If CreateOrder changes how it reads a customer tomorrow, no other slice breaks.
The trap to avoid: the dumping ground
There is one common mistake worth naming clearly. The Common or Shared folder can slowly turn into a dumping ground. Someone adds a DTO there "just for now." Someone else adds a helper. Soon every slice imports from Common, and your slices are quietly glued together again. You are back to the tangled layered design you tried to leave.
To avoid this, keep two habits. First, each slice should define its own request and response shapes, even if they look similar to another slice. Second, only put something in Common if it is truly technical and truly used everywhere, like a pipeline behavior or a Result type. If you are unsure, leave it in the slice. A little repeated code is cheaper than the wrong shared code.
A worked example, end to end
Let us walk through one request to see every kind of shared code in action. A user sends a request to create an order.
A create-order request, end to end
Steps
Request in
User posts new order
Logging behavior
Shared plumbing logs it
Validation behavior
Shared pipeline checks input
Slice handler
Feature-only logic runs
Domain rule
Money value object adds totals safely
Database
Shared context saves the order
In that single trip, the request touched shared logging, a shared validation pipeline, the slice's own handler, a shared domain rule, and the shared database context. Yet the CreateOrder slice never called another feature. Every shared piece was either technical plumbing or a true domain rule. That is the balance VSA is aiming for.
When to bend the rules
No rule fits every case. Here are a few honest exceptions.
If your app is tiny, a heavy Domain folder and many pipeline behaviors may be overkill. Start simple. Add structure only when the pain of not having it is real.
If two slices share a rule that is clearly stable and central to your business, you may extract it before you hit three copies. Judgement matters more than a strict count. The rule of three is a guide, not a law.
If you find yourself fighting the framework to keep slices apart, step back. The goal is low coupling and easy change, not purity for its own sake. A small, sensible shared helper beats a clever abstraction that nobody understands.
Quick recap
- A slice is one full feature, kept together from request to database. Organise by feature, not by technical layer.
- "Shared" hides three kinds of code: technical plumbing, cross-cutting behavior, and business logic. Treat each one differently.
- Technical plumbing, like the database context, logger, and HTTP clients, is shared freely and set up once at startup.
- Cross-cutting concerns, like logging and validation, live in pipeline behaviors or middleware so you write them once.
- Business logic is shared carefully. Use the rule of three and put truly shared rules in a
Domainfolder of entities and value objects. - Most "sharing" is just reading the same data. Let each slice read the database directly instead of calling other features.
- Watch out for the
Commondumping ground. A little duplication is cheaper than the wrong shared code. - Tools like MediatR and MassTransit are now commercially licensed for larger companies. The pipeline idea matters more than any one library.
References and further reading
- Vertical Slice Architecture: Where Does the Shared Logic Live? — Milan Jovanović
- Vertical Slice Architecture in .NET — Complete Guide — Milan Jovanović
- Vertical Slice Architecture — Jimmy Bogard
- Vertical Slice Architecture: The Best Ways to Structure Your Project — Anton DevTips
Related Posts
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.
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.
Balancing Cross-Cutting Concerns in Clean Architecture (.NET)
Learn how to handle logging, validation, caching, and security in Clean Architecture with .NET, using simple words, diagrams, and real code examples.
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.
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.
Migrating a Modular Monolith to Microservices in .NET
A simple, friendly guide to moving a .NET modular monolith to microservices using the strangler fig pattern, YARP, clear boundaries, and safe steps.