Skip to main content
SEMastery
Architectureintermediate

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.

12 min readUpdated April 23, 2026

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.

Figure 1: Each slice keeps its own code together. They do not call into each other.

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 duplicationIs it a problem?What to do
Two slices map a database row to a response the same wayUsually notLeave it. It is shallow and may grow apart
Three slices do the exact same validation stepYes, a real patternExtract to a shared behavior or helper
Every handler logs the same wayYes, but it is infrastructureUse a pipeline behavior, not copy-paste
Two features both calculate "order total" with the same business ruleBe carefulShare 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

See similar code
Merge into shared class
Needs differ later
Coupled and stuck

Steps

1

See similar code

Two slices look alike

2

Merge into shared class

Feels DRY and clean

3

Needs differ later

One feature must change

4

Coupled and stuck

Change breaks the other

Premature sharing trades a little repetition for a lot of coupling.

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.

Figure 2: Wait for the third repetition before you extract.

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
LayerShare it?Reason
Technical infrastructure (logging, caching, auth)Yes, share freelySame for all features, rarely changes
Cross-cutting behavior (validation pipeline)Yes, via behaviorsOne mechanism, many slices opt in
Small mapping or DTO shapingNo, keep in sliceShallow, cheap, may differ
Feature business rulesNo, duplicate if neededTwo 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

Request
Logging behavior
Validation behavior
Slice handler
Response

Steps

1

Request

Comes from endpoint

2

Logging behavior

Logs once for all

3

Validation behavior

Checks the rules

4

Slice handler

Real feature work

One pipeline, many slices, zero repeated plumbing in handlers.

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.

Figure 3: Many slices call one shared infrastructure service.

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.

Figure 4: A shared base class pulls every slice back into one coupled center.

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?

Spot repeat
Infrastructure?
3+ copies?
Same true reason?
Decide

Steps

1

Spot repeat

You wrote it again

2

Infrastructure?

If yes, share it

3

3+ copies?

If no, wait

4

Same true reason?

Not just same shape

5

Decide

Extract or leave

A calm checklist beats a reflex to merge everything.
  1. Is this infrastructure (logging, caching, email)? If yes, share it through a behavior or a service.
  2. Do I have three or more copies? If not, wait. Let it sit.
  3. Do the copies repeat for the same true reason, not just the same shape? If they might diverge, keep them apart.
  4. 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

Related Posts