Clean Architecture vs Vertical Slice: Pragmatism Over Dogma in Modern Software Design
Why This Article Is Part of My Portfolio Journey
This piece is part of my effort to build a public engineering portfolio focused on sustainable architecture, engineering judgment, and long-term system design.
Rather than showcasing isolated code samples, I'm documenting how I think about system design, trade-offs, and real-world engineering decisions β because architecture is as much about reasoning as it is about implementation.
This article represents the type of engineering thinking I aim to bring into the systems I build.
Pragmatism over Dogma: Engineering Judgment in Modern Software Architecture
This article isn't about choosing Clean Architecture or Vertical Slice as the βcorrectβ approach.
It's about understanding how engineering evolves β from structure and theory, through judgment and balance, toward pragmatic delivery.
Clean Architecture and Vertical Slice represent two ends of that spectrum.
The real skill isn't choosing sides β it's knowing how to balance them responsibly in real systems.
Software Architecture Patterns: Clean Architecture vs Vertical Slice Architecture β A Comprehensive Analysis
Author: Harry Lo
Date: January 12, 2026
Document Purpose: Research and analysis comparing architectural approaches for modern software development, with a focus on extensibility, maintainability, and avoiding technical debt.
Copyright Notice: Β© 2026 Harry Lo. This work is licensed under Creative Commons Attribution 4.0 International License (CC BY 4.0). You are free to share and adapt this material with proper attribution.
Table of Contents
1. Architectural Comparison Overview
1.1 Clean Architecture
Definition: A layered architecture emphasizing separation of concerns with core business logic at the center, surrounded by concentric layers for application operations, interfaces, and external systems.
Strengths
- Long-term Maintainability: Core business logic isolated from external dependencies (frameworks, databases, UI)
- High Testability: Business rules can be tested independently without infrastructure
- Technology Independence: Easy to swap databases, frameworks, or external services
- Clear Separation of Concerns: Defined layers with strict dependency rules (dependencies point inward)
- Complex Business Logic: Ideal for rich domain models and complex rules
- Team Scalability: Clear boundaries help new developers understand structure
Weaknesses
- High Initial Overhead: Requires significant upfront design and setup
- Over-engineering Risk: Can be overkill for simple applications
- Boilerplate Code: More interfaces, abstractions, and mapping between layers
- Steep Learning Curve: Team needs to understand SOLID principles, dependency inversion
- Potential Performance Overhead: Multiple layers can introduce latency
Best Suited For
- Large, complex enterprise applications
- Long-term projects (3+ years lifespan)
- Systems with complex business rules
- Projects requiring multiple integrations
- Teams with experienced architects
1.2 Vertical Slice Architecture
Definition: Organizes code around specific features or use cases, treating each as a self-contained βsliceβ spanning all necessary layers from UI to database.
Strengths
- Rapid Development: Features can be built end-to-end quickly
- Feature-based Organization: All code for a feature lives together (UI β DB)
- Team Parallelization: Multiple teams can work on different slices with minimal conflicts
- Reduced Cognitive Load: Developers only need to understand one slice at a time
- Agile-friendly: Aligns perfectly with user stories and sprint planning
- Early Risk Detection: Complete features reveal integration issues sooner
- Lower Initial Complexity: Easier to start compared to Clean Architecture
Weaknesses
- Code Duplication: Validation, error handling, and other logic may be repeated across slices
- Cross-cutting Concerns: Handling logging, security, caching consistently requires discipline
- Potential Inconsistency: Different slices might implement similar features differently
- Learning Curve: Shift in mindset from horizontal layers to vertical features
- Scaling Complexity: Managing dependencies between slices becomes challenging as they increase
Best Suited For
- Agile/Scrum teams with frequent releases
- Microservices or modular monoliths
- Projects with independent features
- Startups and MVP development
- Teams that value speed over long-term abstractions
1.3 Key Differences
| Aspect | Clean Architecture | Vertical Slice Architecture |
|---|---|---|
| Organization | By technical responsibilities (layers) | By business features (slices) |
| Dependencies | Strict inward dependency rules | Feature-specific dependencies |
| Development Approach | Centralized business logic | Distributed across features |
| Project Size | Large, complex | Small to medium |
| Timeline | Long-term (years) | Short to medium |
| Team Experience | Senior developers | Mixed experience |
| Business Complexity | High domain complexity | Feature-focused |
| Change Frequency | Stable requirements | Frequent pivots |
| Development Speed | Slower initial, faster later | Fast throughout |
| Maintainability | Layer reuse, separation | Feature independence |
1.4 Decision Matrix
Choose Clean Architecture when:
- Building large-scale applications where long-term maintainability is paramount
- Complex business rules require strict separation
- Technology stack may change over time
- Multiple teams need clear architectural boundaries
- Enterprise requirements demand high flexibility
Choose Vertical Slice when:
- Rapid development and deployment are priorities
- Features are relatively independent
- Agile methodologies are core to workflow
- Team prefers feature-complete iterations
- Startup or MVP environment
Consider Hybrid when:
- Want feature-based organization with engineering rigor
- Need some shared domain logic
- Cross-cutting concerns require centralization
- Long-term project but with agile delivery
2. Extensibility and Feature Addition
2.1 The βJust Add a Sliceβ Advantage
One of Vertical Slice Architecture's strongest benefits is the ability to add new features by simply creating a new slice without touching existing code.
Example Structure
Features/
βββ UserRegistration/ β Existing
βββ OrderProcessing/ β Existing
βββ PaymentHandling/ β Existing
βββ ProductReviews/ β NEW! Just add this slice
βββ AddReview.cs
βββ GetReviews.cs
βββ DeleteReview.cs
βββ Models/
Why This Is Powerful
1. Zero Impact on Existing Code
- You don't touch other slices
- No risk of breaking existing features
- Each slice is independent
2. Parallel Development
- Team A works on βProduct Reviewsβ
- Team B works on βLoyalty Programβ
- Zero merge conflicts
3. Faster Onboarding
- New developers can study one complete example slice
- Then copy the pattern for new features
- Complete flow visible in one location
4. Progressive Complexity
- Start simple: new slice = new feature
- Grow organically: add more slices as needed
- No upfront architectural decisions required
5. Easy Feature Flags & A/B Testing
// Enable/disable entire features easily
if (featureFlags.IsEnabled("ProductReviews"))
{
services.AddProductReviewsSlice();
}
2.2 Comparison with Clean Architecture
In Clean Architecture, adding a new feature requires touching multiple layers:
Domain/
βββ Entities/
βββ Review.cs β Add here
Application/
βββ UseCases/
βββ AddReview/
β βββ AddReviewUseCase.cs β Add here
β βββ IReviewRepository.cs β Add here
βββ GetReviews/
βββ GetReviewsUseCase.cs β Add here
Infrastructure/
βββ Repositories/
βββ ReviewRepository.cs β Add here
Presentation/
βββ Controllers/
βββ ReviewsController.cs β Add here
Challenges:
- Navigate between 4-5 different folders
- Each layer needs awareness of the new feature
- More places for things to go wrong
- Harder to see the complete feature at once
2.3 Real-World Scenarios
Scenario: E-commerce Platform Evolution
Adding features over 6 months:
With Vertical Slice:
Features/
βββ ProductReviews/ β Sprint 1: Just add this
βββ Wishlist/ β Sprint 2: Just add this
βββ GiftCards/ β Sprint 3: Just add this
βββ LoyaltyPoints/ β Sprint 4: Just add this
βββ ProductComparison/ β Sprint 5: Just add this
βββ RecentlyViewed/ β Sprint 6: Just add this
Each sprint = one new folder. Clean. Isolated. Simple.
With Clean Architecture:
Every sprint requires updating:
- Domain layer (entities, value objects)
- Application layer (use cases, interfaces)
- Infrastructure layer (repositories, services)
- Presentation layer (controllers, DTOs)
Netflix Example
The Netflix API famously uses a similar approach:
- Each team owns complete feature slices
- Teams deploy independently
- Minimal coordination needed
- Can add hundreds of features without architectural paralysis
2.4 Managing Slice Dependencies
Concern: Code Duplication
Problem: Each slice might duplicate validation logic, error handling, etc.
Solutions:
// Shared behaviors via MediatR pipelines
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
// All slices automatically get validation!
}
// Shared abstractions
Common/
βββ Behaviors/
β βββ ValidationBehavior.cs
β βββ LoggingBehavior.cs
β βββ TransactionBehavior.cs
βββ Abstractions/
βββ IRepository<T>.cs
Concern: Slices Need to Communicate
Problem: Product Reviews needs Product data
Solutions:
// Option 1: Direct dependency (acceptable for queries)
Features/ProductReviews/
βββ GetReviewsQuery.cs
// Can query Products directly
// Option 2: Events (for decoupling)
Features/OrderProcessing/
βββ OrderCompleted.cs β raises event
Features/LoyaltyPoints/
βββ OrderCompletedHandler.cs β listens for event
Concern: Shared Domain Logic
Problem: Multiple slices need same business rules
Solution:
Core/
βββ Domain/
βββ Product.cs
βββ Order.cs
βββ Rules/ β Shared business rules
βββ PricingRules.cs
Features/
βββ OrderProcessing/ β Uses PricingRules
βββ QuoteGeneration/ β Uses PricingRules
3. Documentation and Knowledge Management
3.1 Co-located Documentation Benefits
Documentation becomes significantly easier with Vertical Slice Architecture because docs can live right next to the code they describe.
Co-located Structure
Features/
βββ ProductReviews/
β βββ README.md β Feature-specific docs!
β βββ AddReview.cs
β βββ GetReviews.cs
β βββ DeleteReview.cs
βββ OrderProcessing/
β βββ README.md β All order docs here!
β βββ CreateOrder.cs
β βββ CancelOrder.cs
βββ PaymentHandling/
βββ README.md β Payment docs here!
βββ ProcessPayment.cs
Benefits:
- Docs live right next to the code they describe
- When you work on a feature, docs are immediately visible
- No hunting through multiple folders or wikis
Self-Documenting Structure
Each slice tells a complete story in one place. Everything you need to know about βProduct Reviewsβ is in ONE location.
3.2 Documentation Templates
Create a standard template for all slices to ensure consistency:
_FEATURE_TEMPLATE/README.md
# [Feature Name]
## Overview
[Brief description]
## User Stories
- As a [user], I want to [action], so that [benefit]
## Use Cases / Commands / Queries
- [ ] Command/Query name - Description
## API Endpoints
- [ ] METHOD /path - Description
## Business Rules
- [ ] Rule description
## Dependencies
- **Requires:** [Other features needed]
- **Publishes:** [Events this feature raises]
- **Consumes:** [Events this feature listens to]
## Database Changes
- [ ] Tables/Collections affected
## Configuration
- [ ] Settings required
## Testing
- [ ] Test scenarios
## Deployment Notes
- [ ] Special considerations
Every new slice copies this template = consistent documentation across all features!
3.3 Living Documentation Practice
Documentation updates become part of feature work:
Pull Request Structure:
Features/GiftCards/
βββ README.md β Updated as part of PR
βββ PurchaseGiftCard.cs
βββ RedeemGiftCard.cs
βββ CheckBalance.cs
Code Review Checklist:
- β Code implements feature
- β Tests pass
- β README.md is updated
Because docs are co-located, developers naturally update them!
3.4 Onboarding Advantages
Comparison: Understanding βProduct Reviewsβ
Vertical Slice Approach:
"Open Features/ProductReviews/ folder and read the README"
Done in 5 minutes! β
Clean Architecture Approach:
"Read these docs:
1. Domain/Entities.md (Review entity)
2. Application/UseCases.md (Review use cases)
3. Infrastructure/Repositories.md (Review storage)
4. API/Endpoints.md (Review API)
5. Domain/Events.md for ReviewSubmitted event
6. Application/EventHandlers.md for handlers"
Takes 30+ minutes π
Documentation Comparison Table
| Aspect | Clean Architecture | Vertical Slice |
|---|---|---|
| Doc location | Scattered across layers | Co-located with feature |
| Finding docs | Search multiple folders | One folder = one feature |
| Completeness | Often incomplete | Easier to keep complete |
| Onboarding time | 30-60 minutes per feature | 10-20 minutes per feature |
| Update likelihood | Often forgotten | Updated with PRs |
| Template reuse | Hard to standardize | Easy template per slice |
| Feature overview | Requires reading multiple docs | Single README |
4. Helper Functions and Technical Debt
4.1 The Helper Function Decay Pattern
A common anti-pattern occurs when developers try to make a helper function serve multiple purposes by repeatedly modifying it.
Evolution of Decay
// Stage 1: Initial extraction (seems good!)
public static string FormatUserName(User user)
{
return $"{user.FirstName} {user.LastName}";
}
// Stage 2: One place needs something slightly different
public static string FormatUserName(User user, bool includeTitle = false)
{
if (includeTitle)
return $"{user.Title} {user.FirstName} {user.LastName}";
return $"{user.FirstName} {user.LastName}";
}
// Stage 3: Another place needs another variation
public static string FormatUserName(User user, bool includeTitle = false, bool lastNameFirst = false)
{
if (lastNameFirst && includeTitle)
return $"{user.Title} {user.LastName}, {user.FirstName}";
if (lastNameFirst)
return $"{user.LastName}, {user.FirstName}";
if (includeTitle)
return $"{user.Title} {user.FirstName} {user.LastName}";
return $"{user.FirstName} {user.LastName}";
}
// Stage 4: TECHNICAL DEATH π
public static string FormatUserName(
User user,
bool includeTitle = false,
bool lastNameFirst = false,
bool includeMiddleName = false,
bool abbreviateMiddle = false,
bool uppercase = false,
NameFormat format = NameFormat.Standard)
{
// 50+ lines of conditional logic
// Nobody dares to change it anymore
// Everyone is afraid it will break something
// New developers avoid touching it
}
4.2 Why This Leads to Technical Death
1. High Cognitive Load
- Developers must understand ALL variations to make ANY change
- The function serves too many masters
- Testing becomes exponentially complex
2. Shotgun Surgery
- A bug in one use case might require touching the shared helper
- That change could break OTHER use cases
- Ripple effects across the codebase
3. Fear-Driven Development
- βDon't touch it, it worksβ
- Developers add MORE parameters instead of refactoring
- The problem compounds over time
4. Loss of Intent
- The original purpose is obscured by all the variations
- Code becomes about βwhatβ not βwhyβ
- Business logic is hidden in boolean flags
The Root Cause: Misapplied DRY Principle
The Mistake:
βWe have similar code in 3 places, let's extract a helper function!β
The Reality:
- Code that LOOKS similar might serve DIFFERENT purposes
- Different contexts have different reasons to change
- βDuplication is far cheaper than the wrong abstractionβ β Sandi Metz
4.3 When Duplication Is Better Than Abstraction
The βRule of Threeβ
- First occurrence: Write the code inline
- Second occurrence: Still write it inline (just 2 places)
-
Third occurrence: NOW consider extracting IF:
- β The logic is truly identical
- β It changes for the SAME reasons
- β It serves the SAME purpose
- β It has the SAME business context
Example: When NOT to Extract
BAD Extraction (similar code, different purposes):
// DON'T extract this - different purposes!
// Invoice: needs title for legal reasons
FormatUserName(user, includeTitle: true);
// Welcome email: needs friendly name for UX
FormatUserName(user, includeTitle: false);
// These LOOK similar but will evolve differently!
GOOD Extraction (truly identical, same purpose):
// DO extract this - same purpose
public static string ToUpperSnakeCase(string input)
{
return string.Join("_", input.Split(' '))
.ToUpper();
}
// Used consistently for environment variable names
var dbVar = ToUpperSnakeCase("database url");
var apiVar = ToUpperSnakeCase("api key");
How Vertical Slice Prevents This
Features/
βββ CustomerInvoice/
β βββ FormatCustomerName.cs
β // Formats: "Title FirstName LastName" for invoices
β // This is for LEGAL/BILLING purposes
β
βββ EmailNotifications/
β βββ FormatFriendlyName.cs
β // Formats: "FirstName" for friendly emails
β // This is for USER EXPERIENCE
β
βββ AdminUserList/
βββ FormatDisplayName.cs
// Formats: "LastName, FirstName" for sorting
// This is for ADMINISTRATIVE purposes
Benefits:
- β Each slice has its OWN formatting logic
- β Changes in one don't affect others
- β Intent is clear from context
- β If they diverge over time, no problem!
- β No boolean flags or complex conditionals
4.4 Better Alternatives
Option 1: Multiple Specific Functions
// Clear purpose, no flags
public static string FormatForInvoice(User user)
=> $"{user.Title} {user.FirstName} {user.LastName}";
public static string FormatForEmail(User user)
=> user.FirstName;
public static string FormatForAdminList(User user)
=> $"{user.LastName}, {user.FirstName}";
Option 2: Strategy Pattern
public interface INameFormatter
{
string Format(User user);
}
public class InvoiceNameFormatter : INameFormatter
{
public string Format(User user)
=> $"{user.Title} {user.FirstName} {user.LastName}";
}
public class EmailNameFormatter : INameFormatter
{
public string Format(User user) => user.FirstName;
}
Option 3: Extension Methods (Context-Specific)
// In Features/Invoice/
public static class UserExtensions
{
public static string ToInvoiceName(this User user)
=> $"{user.Title} {user.FirstName} {user.LastName}";
}
// In Features/Email/
public static class UserExtensions
{
public static string ToFriendlyName(this User user)
=> user.FirstName;
}
Option 4: Keep It Inline (Often the Best!)
// In Features/Invoice/GenerateInvoice.cs
var customerName = $"{user.Title} {user.FirstName} {user.LastName}";
// In Features/Email/SendWelcome.cs
var greeting = $"Hi {user.FirstName}!";
// Just 1 line each - no need to extract!
Red Flags: When Your Helper Function Is Dying
π© More than 3 boolean parameters
FormatUserName(user, true, false, true, false) // What does this mean??
π© Parameter names with βOrβ or βAlsoβ
FormatUserName(user, includeTitleOrPrefix: true)
π© Comments explaining parameter combinations
// Use includeTitle=true and lastNameFirst=false for invoices
// Use includeTitle=false and lastNameFirst=true for admin
π© Nested conditionals based on parameters
π© The function name is generic
ProcessData()
HandleStuff()
DoTheThing()
5. Open-Closed Principle in Practice
5.1 OCP Applied to Helper Functions
Open-Closed Principle:
βSoftware entities should be open for extension, but closed for modificationβ β Bertrand Meyer
In plain English:
- You should be able to ADD new behavior (open for extension)
- WITHOUT changing existing code (closed for modification)
The Bad Pattern (Violates OCP)
// Initial version
public static string FormatUserName(User user)
{
return $"{user.FirstName} {user.LastName}";
}
// β Modification #1 - VIOLATES OCP
public static string FormatUserName(User user, bool includeTitle = false)
{
if (includeTitle)
return $"{user.Title} {user.FirstName} {user.LastName}";
return $"{user.FirstName} {user.LastName}";
}
// β Modification #2 - VIOLATES OCP AGAIN
public static string FormatUserName(User user, bool includeTitle = false, bool lastNameFirst = false)
{
// More conditional logic...
}
Why this violates OCP:
- Every new requirement MODIFIES the existing function
- Existing callers are at risk (regression bugs)
- The function is NOT closed for modification
- You're changing working code repeatedly
The Good Pattern (Follows OCP)
Multiple Specific Functions:
// Original function - NEVER modified again β
public static string FormatUserName(User user)
{
return $"{user.FirstName} {user.LastName}";
}
// New requirement? ADD new function, don't modify existing β
public static string FormatUserNameWithTitle(User user)
{
return $"{user.Title} {user.FirstName} {user.LastName}";
}
// Another requirement? ADD another function β
public static string FormatUserNameLastFirst(User user)
{
return $"{user.LastName}, {user.FirstName}";
}
// Extension through ADDITION, not MODIFICATION β
Strategy Pattern (Classic OCP):
// Interface defines the contract - rarely changes β
public interface IUserNameFormatter
{
string Format(User user);
}
// Original formatter - NEVER modified β
public class StandardNameFormatter : IUserNameFormatter
{
public string Format(User user)
=> $"{user.FirstName} {user.LastName}";
}
// New requirement? ADD new class (extension) β
public class TitledNameFormatter : IUserNameFormatter
{
public string Format(User user)
=> $"{user.Title} {user.FirstName} {user.LastName}";
}
5.2 Violations and Warning Signs
OCP as a Decision Tool
βAm I MODIFYING existing code, or ADDING new code?β
| Scenario | Violates OCP? | Technical Debt Risk |
|---|---|---|
| Adding boolean parameter to helper | β YES | HIGH |
| Adding conditional logic to helper | β YES | HIGH |
| Creating new specific function | β NO | LOW |
| Creating new strategy class | β NO | LOW |
| Inline code in feature slice | β NO | NONE |
Red Flags for OCP Violations
π© Growing parameter list
FormatUserName(user, flag1, flag2, flag3, flag4...)
// Each new param = OCP violation
π© Increasing cyclomatic complexity
if (flag1)
if (flag2)
if (flag3)
// OCP violated repeatedly
π© Fear of changing the function
// "What if I break something?"
// This fear means OCP was already violated
π© Function serves multiple βmastersβ
// Used by: Invoice, Email, Admin, Report, Dashboard
// Each master has different needs
// Function can't be "closed" because needs conflict
5.3 Extension Over Modification
Real-World Example: Email Formatting Evolution
The Wrong Way (Violates OCP):
// Week 1: Initial implementation
public static string FormatEmail(string template, User user)
{
return template.Replace("{NAME}", user.FirstName);
}
// Week 3: "We need HTML emails too!"
// β MODIFYING existing function
public static string FormatEmail(string template, User user, bool isHtml = false)
{
var name = user.FirstName;
if (isHtml)
name = $"{name}";
return template.Replace("{NAME}", name);
}
// Week 8: Now it's unmaintainable
public static string FormatEmail(
string template,
User user,
bool isHtml = false,
bool includeLastName = false,
string locale = "en-US")
{
// 50 lines of conditional logic
// OCP completely violated
}
The Right Way (Follows OCP):
// Base abstraction - CLOSED for modification β
public interface IEmailFormatter
{
string Format(string template, User user);
}
// Week 1: Text email formatter
public class TextEmailFormatter : IEmailFormatter
{
public string Format(string template, User user)
=> template.Replace("{NAME}", user.FirstName);
}
// Week 3: HTML email formatter - ADDED, not modified β
public class HtmlEmailFormatter : IEmailFormatter
{
public string Format(string template, User user)
=> template.Replace("{NAME}", $"{user.FirstName}");
}
// Week 8: Localized formatter - ADDED, not modified β
public class LocalizedEmailFormatter : IEmailFormatter
{
private readonly string _locale;
public string Format(string template, User user)
{
var name = _locale switch
{
"ja-JP" => $"{user.LastName} {user.FirstName}",
"en-US" => $"{user.FirstName} {user.LastName}",
_ => user.FirstName
};
return template.Replace("{NAME}", name);
}
}
What changed over 8 weeks?
- β Text email formatter? NEVER modified
- β HTML email formatter? NEVER modified
- β Original interface? NEVER modified
- β System capabilities? Expanded through extension
5.4 Decision Framework
When Considering Modifying a Helper Function
Is the function used in multiple places?
ββ NO β β
Safe to modify (low coupling)
ββ YES β Is this a bug fix or refactor (same behavior)?
ββ YES β β
Probably safe to modify
ββ NO β Are you adding conditional logic/parameters?
ββ YES β β STOP! Violates OCP
β β Consider: new function or strategy pattern
ββ NO β Are all callers okay with the change?
ββ YES β β οΈ Modify with caution
ββ NO β β STOP! Violates OCP
β Definitely use extension approach
When Modification Is Actually OK
OCP isn't absolute. Modification is acceptable when:
1. Fixing a Bug
// β
OK to modify - it was WRONG before
public static string FormatUserName(User user)
{
// Was: return $"{user.FirstName} {user.LastName}";
return $"{user.FirstName?.Trim()} {user.LastName?.Trim()}";
}
2. Refactoring Internal Implementation
// β
OK to modify - external behavior unchanged
public static string FormatUserName(User user)
{
// Optimized implementation, same result
return string.Join(" ", user.FirstName, user.LastName);
}
3. The Function Has Only ONE Caller
// β
OK to modify - no other dependencies
// Features/Invoice/InvoiceHelpers.cs
private static string FormatForInvoice(User user)
{
// Only used by Invoice feature, safe to evolve
}
4. Breaking Change Is Intentional & Communicated
// β
OK to modify - with proper versioning
[Obsolete("Use FormatUserNameV2 instead")]
public static string FormatUserName(User user) { }
public static string FormatUserNameV2(User user) { }
6. Hybrid Approach
Many modern teams combine the best of both architectures:
Structure
Features/
βββ UserRegistration/
β βββ Commands/
β βββ Validators/
β βββ Handlers/
β βββ Models/
βββ OrderProcessing/
βββ PaymentHandling/
Common/
βββ Behaviors/ (cross-cutting concerns)
βββ Infrastructure/
Benefits
- Use Vertical Slices for feature organization (folder structure)
- Apply Clean Architecture principles within each slice (separation of concerns, dependency inversion)
- Centralize cross-cutting concerns using middleware/pipelines (MediatR pattern in .NET)
- Share common domain logic in a core layer
OCP + Vertical Slice = Natural Fit
Vertical Slice Architecture naturally encourages OCP:
Features/
βββ InvoiceGeneration/ β Closed for modification
βββ WelcomeEmail/ β Closed for modification
βββ AdminDashboard/ β Closed for modification
βββ CustomerReport/ β NEW! Extension via addition
This is OCP at the feature level!
7. Recommendations and Conclusions
Key Principles
-
βDuplication is far cheaper than the wrong abstractionβ β Sandi Metz
- Don't rush to extract helper functions
- Different contexts may have different reasons to change
- Duplication across feature boundaries is acceptable and healthy
-
βOptimize for change, not for avoiding duplicationβ
- Make it easy to understand and modify
- Loose coupling is more valuable than DRY
- Consider future evolution, not just current state
-
Open-Closed Principle is your guide
- If you can't extend without modifying, your abstraction is wrong
- Repeated modification of a helper function is a red flag
- Extension through addition prevents technical debt
-
Vertical Slice naturally prevents common anti-patterns
- Feature isolation prevents over-sharing
- Co-location improves understanding
- Independent evolution reduces coupling
Practical Guidelines
For Helper Functions
Before extracting, ask:
- β Does this code change for the SAME reason?
- β Is it truly identical, or just similar?
- β Will future requirements diverge these use cases?
- β Is the extraction making the code EASIER to understand?
If you answered βNoβ or βMaybeβ to any β DON'T extract yet.
When duplication is better:
- Code serves different business purposes
- Different contexts have different evolution paths
- Extracting would introduce coupling
- The code is simple enough to keep inline
For Architecture Choice
Choose Clean Architecture when:
- Building enterprise systems with complex business rules
- Long-term maintainability (3+ years) is critical
- Technology stack may change
- Team has senior architects
Choose Vertical Slice when:
- Rapid feature delivery is priority
- Features are relatively independent
- Agile/iterative development
- Team values pragmatism over purity
Consider Hybrid when:
- Want feature organization with engineering discipline
- Need some shared domain logic
- Long-term project with agile delivery
For Documentation
Vertical Slice advantages:
- Co-locate docs with code
- Use templates for consistency
- Update docs as part of feature PRs
- One folder = complete feature understanding
The Core Insight
Helper Function Decay β OCP Violation β Technical Death
The pattern is clear:
- Helper function created (seems reusable)
- New requirement β function MODIFIED (violates OCP)
- Another requirement β MODIFIED again (repeated OCP violation)
- Function becomes complex, fragile, scary (technical death)
OCP violation is the root cause of technical death in helper functions.
Final Thoughts
- Architecture is about trade-offs, not absolutes
- Choose patterns that fit your team and project
- Be willing to duplicate code when it prevents coupling
- Extend through addition, not modification
- Keep features independent and well-documented
- Trust the Open-Closed Principle as your guide
Mature engineering isn't defined by the frameworks we use, but by the decisions we make. Clean Architecture and Vertical Slice are just tools. What matters is the judgment to apply them wisely and the discipline to avoid turning principles into dogma.
βMake it work, make it right, make it fastβ β Kent Beck
Start simple, refactor when patterns emerge, and always prefer the simplest solution that could work.
References
Books and Publications
-
Martin, Robert C. (2017). Clean Architecture: A Craftsman's Guide
to Software Structure and Design. Prentice Hall. -
Meyer, Bertrand (1988). Object-Oriented Software Construction.
Prentice Hall. -
Metz, Sandi (2016). The Wrong Abstraction. Sandi Metz Blog.\
Available at:
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction -
Beck, Kent (2004). Extreme Programming Explained: Embrace Change
(2nd ed.). Addison-Wesley Professional. -
Hunt, Andrew & Thomas, David (1999). The Pragmatic Programmer: From
Journeyman to Master. Addison-Wesley.
Architecture & Design Articles
-
Bogard, Jimmy (2018). Vertical Slice Architecture.\
Available at:
https://www.jimmybogard.com/vertical-slice-architecture/ -
JovanoviΔ, Milan (2023). Vertical Slice Architecture in Modern
Applications.\
Available at:
https://www.milanjovanovic.tech/blog/vertical-slice-architecture -
Ozkaya, Mehmet (2023). Vertical Slice Architecture and Comparison
with Clean Architecture. Medium.\
Available at:
https://mehmetozkaya.medium.com/vertical-slice-architecture-and-comparison-with-clean-architecture-76f813e3dab6 -
Ngom, Issa (2024). Clean Architecture vs Vertical Slice. LinkedIn
Pulse.\
Available at:
https://fr.linkedin.com/pulse/clean-architecture-vs-vertical-slice-issa-ngom-76bhe -
Asmak9 (2025). Vertical Slice Architecture in ASP.NET Core.\
Available at:
https://www.asmak9.com/2025/07/vertical-slice-architecture-in-aspnet.html -
Onishi, Yuki (2024). Benefits and Drawbacks of Adopting Clean
Architecture. Dev.to.\
Available at:
https://dev.to/yukionishi1129/benefits-and-drawbacks-of-adopting-clean-architecture-2pd1 -
Fowler, Martin (2018). Modular Monoliths.\
Available at: https://martinfowler.com/bliki/ModularMonolith.html
Official Documentation & Principles
-
Microsoft Learn (2025). Extension Methods (C# Programming Guide).\
Available at:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods -
Martin, Robert C. (n.d.). The Open-Closed Principle. The
Principles of OOD.\
Available at:
https://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Community Resources
- Monday.com (2024). Vertical Slice Architecture.\
Available at: https://monday.com/blog/rnd/
About the Author
Harry Lo is a passionate software developer focused on improving software development environments and engineering practices. Through experience, he has observed that even good principles can lead to negative outcomes when misapplied or overused. His work is driven by a desire to find the right balance between structure and pragmatism in building sustainable, developer-friendly systems.
For questions, feedback, or discussions about this article, please feel free to reach out.
End of Document