Vertical Slice [[[Architecture](/blog/181874-meetings-one-missing-security-rule-the-tldv-leak-is-an-architecture-lesson)](/blog/single-process-architecture-in-rust-why-we-rejected-microservices-for-an-edge-ai-platform)](/blog/clean-architecture-in-flutter-with-bloc-a-practical-guide) (VSA) shifts the focus from layering (Presentation, Domain, Infrastructure) to feature-based organization. By encapsulating all code required for a specific business requirement—from the API endpoint to the database query—within a single slice, developers gain high cohesion and low coupling. However, this architectural style often triggers concerns regarding the ‘Don’t Repeat Yourself’ (DRY) principle. When multiple features require the same validation logic or data transformation, the temptation to revert to shared layers often threatens the integrity of the slices.
The challenge lies in distinguishing between accidental duplication and shared business invariants. If you find yourself duplicating code across slices, it is rarely a failure of the architecture itself, but rather a sign that your domain boundaries or abstractions require recalibration. Managing this requires a nuanced approach that prioritizes feature autonomy while leveraging common infrastructure where appropriate.
Understanding the Scope of Duplication
In a well-designed vertical slice, duplication is not inherently bad. If two slices share a piece of code, but that code changes for different reasons (different volatility), keeping them separate is actually a feature, not a bug. This is the core tenet of the Single Responsibility Principle applied to architecture: code that changes together should live together.
However, when duplication involves complex business logic or cross-cutting concerns, it creates maintenance overhead. The goal is to extract shared logic into ‘Shared Kernels’ or ‘Common Modules’ only when the logic is truly stable and represents a shared invariant across the entire domain.
Strategic Extraction: The Shared Kernel Pattern
For logic that must be consistent across the entire application—such as specific domain entities, value objects, or core business rules—a Shared Kernel is the appropriate mechanism. In .NET, this is best implemented as a separate project or a shared namespace that is strictly limited in scope.
namespace Core.Shared.Domain;
public record Money(decimal Amount, string Currency) {
public static Money operator +(Money a, Money b) {
if (a.Currency != b.Currency) throw new ArgumentException("Currency mismatch");
return new Money(a.Amount + b.Amount, a.Currency);
}
}
By placing stable, immutable value objects in a shared project, you ensure consistency across slices without forcing features to depend on each other. The key is to keep this layer thin; if it becomes a dumping ground for all shared logic, you have essentially rebuilt a monolithic layer.
Leveraging Mediator Patterns and Behaviors
Cross-cutting concerns like logging, transaction management, or authentication should never be duplicated in each slice. In the .NET ecosystem, the MediatR library provides a powerful way to handle these concerns via pipeline behaviors.
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _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(r => r.Errors).ToList();
if (failures.Any()) throw new ValidationException(failures);
return await next();
}
}
This approach removes the need to manually invoke validation or logging in every single request handler. By injecting these behaviors into the MediatR pipeline, you centralize the execution logic while keeping the actual handler logic inside the slice.
When to Favor Duplication over Abstraction
One of the most difficult lessons in architectural design is knowing when to accept duplication. If two features perform similar database queries but have different performance requirements or evolve at different speeds, creating a shared repository or service will couple them together.
Duplication is far cheaper than the wrong abstraction. If you couple two features prematurely to avoid a few lines of code, you create a rigid system that is difficult to refactor later.
If a piece of code is duplicated across three or four slices, and you notice that modifying one slice now requires you to update the others to avoid breaking changes, only then should you consider refactoring that code into a shared service. If the code is static and rarely changes, duplication is often the most maintainable path.
Conclusion
Vertical Slice Architecture in .NET is not about eliminating all repetition; it is about managing dependencies. By using Shared Kernels for immutable domain concepts and MediatR pipeline behaviors for cross-cutting concerns, you can maintain a clean, organized codebase without falling into the trap of over-engineering.
Always evaluate whether a shared abstraction provides actual value or if it simply adds unnecessary coupling. When in doubt, prefer duplication until the pattern becomes clear and the cost of maintenance outweighs the cost of decoupling. This pragmatic approach ensures your slices remain independent, testable, and aligned with the actual business requirements of your application.