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.
Two tiffin boxes that look the same
Imagine two friends, Riya and Arjun, who both carry a tiffin box to school every day.
From the outside, both boxes look almost the same. Both have rice. Both have a little dal. So someone says, "Why carry two boxes? Just share one big box of rice and dal between you."
That sounds clever. But Riya cannot eat spicy food, and Arjun loves extra chili. Riya wants curd, Arjun wants pickle. The day you force them to share one box, every small change becomes a fight. Riya changing her lunch now breaks Arjun's lunch.
Two things that look alike are not always the same thing. This is the heart of avoiding code duplication in Vertical Slice Architecture (VSA). Each slice is like a tiffin box. It keeps its own food together. Sometimes two slices look similar, and you feel a strong urge to merge them. But merging the wrong things creates pain later.
In this guide we will learn when to keep the boxes separate, when it is truly safe to share, and the simple tools .NET gives us to remove the duplication that actually matters.
A quick refresher on Vertical Slice Architecture
In VSA, you organise code by feature, not by technical layer. Each feature, like "Create Order" or "Get Invoice", keeps everything it needs in one folder: the request, the handler, the validation, and the data access.
Because each slice is self-contained, you can add or change a feature by touching one folder. This is great for speed and for teams. But it also means the same small piece of code might appear in many slices. That feeling, "I am writing this again," is what makes people worry about duplication.
The good news is that not all duplication is bad. Let us see the difference.
Not all duplication is the same
There is a famous rule called DRY (Don't Repeat Yourself). It is a good rule, but people often follow it too hard. They see two lines that look alike and rush to merge them.
There is a quieter, wiser rule called WET (Write Everything Twice). It says: it is fine to write something twice. Wait. Only when you see it a third time should you think about pulling it out. Two copies might be a coincidence. Three copies is a real pattern.
| Type of duplication | Is it a problem? | What to do |
|---|---|---|
| Two slices map a database row to a response the same way | Usually not | Leave it. It is shallow and may grow apart |
| Three slices do the exact same validation step | Yes, a real pattern | Extract to a shared behavior or helper |
| Every handler logs the same way | Yes, but it is infrastructure | Use a pipeline behavior, not copy-paste |
| Two features both calculate "order total" with the same business rule | Be careful | Share only if the rule is truly one rule |
The key idea: shallow duplication is cheap, but wrong abstraction is expensive. If you merge two slices and they later need to change in different directions, you are stuck. One feature's change now breaks the other, just like Riya and Arjun's shared tiffin.
The cost of merging too early
Steps
See similar code
Two slices look alike
Merge into shared class
Feels DRY and clean
Needs differ later
One feature must change
Coupled and stuck
Change breaks the other
The rule of three
Here is a simple rule you can carry in your pocket.
Do not extract shared code until three slices truly do the same thing.
Why three? Because two copies are easy to keep in sync and often turn out different later. By the time you have three copies, you have enough evidence that this really is one stable idea, not a coincidence. You can now see the true shape of the pattern, so the abstraction you create is much more likely to be right.
This rule keeps you calm. You stop fighting every small repeat. You save your energy for the duplication that actually hurts.
What is always safe to share
Some code is not really "feature" code at all. It is plumbing. It is the same for every feature and almost never changes per feature. This is technical infrastructure, and it is always safe to share.
Examples of safe-to-share infrastructure:
- Logging and tracing
- Caching
- Authentication and authorization
- Database connection and transaction setup
- Error handling and turning exceptions into HTTP responses
- Validation that runs the same way for every request
| Layer | Share it? | Reason |
|---|---|---|
| Technical infrastructure (logging, caching, auth) | Yes, share freely | Same for all features, rarely changes |
| Cross-cutting behavior (validation pipeline) | Yes, via behaviors | One mechanism, many slices opt in |
| Small mapping or DTO shaping | No, keep in slice | Shallow, cheap, may differ |
| Feature business rules | No, duplicate if needed | Two features often diverge |
So the line is simple. Share the plumbing. Keep the feature logic in the slice. When two features need similar business logic, it is often safer to let each slice keep its own copy.
Tool 1: Pipeline behaviors for cross-cutting concerns
The most powerful tool to remove "every handler does this" duplication is the pipeline behavior. Think of it as a wrapper that runs around every request, like a security guard who checks everyone at the gate so each shop inside does not need its own guard.
Logging, validation, and timing are perfect for this. You write the code once, and it runs for every slice without any slice repeating it.
Here is a small validation behavior. It runs validators before the handler, so no handler needs to call validation by hand.
public sealed class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
=> _validators = validators;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}Now any slice can drop in a validator class, and the behavior takes care of running it. The handler stays clean and focused only on its own job.
A request flowing through behaviors
Steps
Request
Comes from endpoint
Logging behavior
Logs once for all
Validation behavior
Checks the rules
Slice handler
Real feature work
A quick note on tools: the popular MediatR and MassTransit libraries are now commercially licensed for many uses. Pipeline behaviors are a pattern, not a single library. You can build the same wrapper idea with a small hand-written dispatcher, or with a free alternative. The shape of the solution stays the same.
Tool 2: Shared infrastructure services
When several slices need to talk to the same outside system, like sending an email or saving a file, do not copy that code into every slice. Put it behind one small service and let slices call it.
public interface IEmailSender
{
Task SendAsync(string to, string subject, string body, CancellationToken ct);
}
public sealed class SmtpEmailSender : IEmailSender
{
public async Task SendAsync(
string to, string subject, string body, CancellationToken ct)
{
// one place that knows how to send email
// every slice just calls SendAsync
await Task.CompletedTask;
}
}This is safe sharing. Sending email is plumbing. It is the same no matter which feature triggers it. A slice that confirms an order and a slice that resets a password both call the same IEmailSender, and neither one owns the other.
Tool 3: Small, honest helper methods
Sometimes the repeated code is just a tiny calculation or a format. You do not need a behavior or a service. A small static helper or an extension method is enough.
The trick is to keep helpers small and honest. A good helper does one clear thing and has a name that says exactly what it does. It does not try to be a giant base class that every slice must inherit from.
public static class MoneyExtensions
{
public static decimal ApplyDiscount(this decimal price, decimal percent)
{
if (percent is < 0 or > 100)
throw new ArgumentOutOfRangeException(nameof(percent));
return Math.Round(price - (price * percent / 100m), 2);
}
}This is a pure helper. It takes input and returns output. It carries no hidden state. Any slice can use it, and using it does not chain that slice to any other slice. That is the test of a good shared helper: sharing it does not couple the callers to each other.
The trap to avoid: base handlers and generic repositories
Here is the most common mistake. A team sees similar handlers and decides to make a BaseHandler<TRequest, TResponse> that every slice inherits. Or they build a generic IRepository<T> that every slice uses for data access.
It feels clean on day one. But it quietly destroys the main benefit of VSA. Suddenly every slice depends on one base class. A change to that base class can ripple into every feature. You are back to the tightly coupled, horizontal world you were trying to escape. The tiffin boxes have been forced into one shared pot again.
Prefer composition over inheritance here. Instead of inheriting behavior, slices call small services and helpers when they choose to. The slice stays in charge. Nothing is forced on it.
A simple decision path
When you spot repeated code, do not panic and do not merge by reflex. Walk through these questions instead.
Should I extract this duplication?
Steps
Spot repeat
You wrote it again
Infrastructure?
If yes, share it
3+ copies?
If no, wait
Same true reason?
Not just same shape
Decide
Extract or leave
- Is this infrastructure (logging, caching, email)? If yes, share it through a behavior or a service.
- Do I have three or more copies? If not, wait. Let it sit.
- Do the copies repeat for the same true reason, not just the same shape? If they might diverge, keep them apart.
- If I extract this, will the callers become coupled to each other? If yes, think twice.
If you pass all four checks, extract with confidence. If not, a little duplication is the cheaper, safer choice.
A worked example
Say you have three slices that all need to check that the current user owns the order before acting on it: CancelOrder, RefundOrder, and UpdateOrder.
This is three copies (rule of three passes). The reason is the same in all three: ownership. And it is closer to a shared policy than to per-feature business logic. So this is a good candidate to extract into a small shared check.
public interface IOwnershipGuard
{
Task EnsureOwnsOrderAsync(Guid userId, Guid orderId, CancellationToken ct);
}Each of the three slices calls EnsureOwnsOrderAsync at the top of its handler. The check lives in one place, but each slice still owns its own real work. You removed the painful duplication without gluing the features together.
Compare that with a different case: CancelOrder and RefundOrder both build a response object that happens to have the same three fields today. That is only two copies, and it is just shape, not a deep rule. Refunds may soon need a refund reference number that cancels do not have. Here you should leave the two copies alone. Merging them would only create future pain.
Quick recap
- In Vertical Slice Architecture, each slice keeps its own code together, like a personal tiffin box. Some repetition is normal and fine.
- Not all duplication is bad. Shallow repeats are cheap. The expensive mistake is a wrong abstraction that couples features together.
- Follow the rule of three: wait until three slices truly do the same thing before you extract shared code.
- Share the plumbing, keep the feature logic. Infrastructure like logging, caching, auth, and email is always safe to share.
- Use pipeline behaviors for cross-cutting concerns so handlers never repeat logging or validation.
- Use small infrastructure services and honest helper methods for safe reuse that does not couple callers.
- Avoid base handlers and generic repositories. They quietly drag every slice back into one coupled center.
- When in doubt, ask: would extracting this make my slices depend on each other? If yes, a little duplication is the better deal.
References and further reading
- How to Avoid Code Duplication in Vertical Slice Architecture in .NET — Anton DevTips
- Vertical Slice Architecture in .NET — Milan Jovanović
- Vertical Slice Architecture in ASP.NET Core — NDepend Blog
- Vertical Slice Architecture — Jimmy Bogard
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.
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.
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.
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.
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.
N-Layered vs Clean vs Vertical Slice Architecture in .NET
A simple, friendly guide comparing N-layered, Clean, and Vertical Slice architecture in .NET, with diagrams, code, tables, and clear advice on when to pick each one.