Architectural Holy Wars: Why Every Approach Is Right (and Wrong)

Architectural Holy Wars: Why Every Approach Is Right (and Wrong)

Code Rant Chronicles

Are you a fan of Clean Architecture, Vertical Slices, or Hexagonal / Ports and Adapters? Wonderful. Does that mean you should foam at the mouth and trash everyone else’s approach? Probably not—but here we are anyway.

“We should use Clean Architecture by default on all our projects. Just grab that template, and boom—Uncle Bob approves!”

“Clean Architecture is outdated! Vertical Slice Architecture is the future; the rest is garbage!”

Listen: you can mess up any good architecture with your dirty little hands if you don’t understand the why. Let’s begin with the so-called “Clean” Architecture.

Are We Calling Everything Else “Dirty”?

First off, I love how calling it “Clean” implies everything else is a cluttered pigsty. But the real issue is all those “Clean Architecture” templates floating around the internet that do the exact opposite of what the book suggests. No wonder people say, “Ew, CA? No thanks.”

If you actually read the book, you’d know Uncle Bob talked about Screaming Architecture—where your project structure screams the domain (banking, insurance, e-commerce)—not “Controllers,” “DataAccess,” and “Services” slapped everywhere. He also mentioned narrow vertical slices cutting through layers, effectively grouping by use cases.

Now, that’s starting to sound an awful lot like Vertical Slice Architecture (VSA), isn’t it?

CA vs. VSA: More in Common Than You Think

Let’s cut through the marketing hype:

  • Clean Architecture (CA): Separate technical concerns (UI, DB, frameworks) from business logic.

  • Vertical Slice Architecture (VSA): Group each feature or request in one cohesive spot, ignoring or minimizing layer-obsession.

They might look different, but both aim at separation of concerns—just from different angles. And if you’re a CA junkie who never ships your BusinessLogic or DataAccess layers independently, ask yourself why you really need them as separate projects. Because it seems you might just be layering for layering’s sake.

Example: Keep It Simple with a Narrow Slice

So you want to keep your code structure sane? Here’s a quick snippet showcasing a “vertical” approach without going layer-crazy:

public record OrderRequest(int UserId, decimal Amount);

public class PlaceOrderHandler
{
    private readonly IOrderRepository _orderRepository;

    public PlaceOrderHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public Result Handle(OrderRequest request)
    {
        // Domain validation
        if (request.Amount <= 0)
        {
            return Result.Fail("Invalid order amount.");
        }

        // Construct domain entity
        var order = new Order
        {
            UserId = request.UserId,
            Amount = request.Amount,
            Status = OrderStatus.Pending
        };

        // Persist to DB (infrastructure detail)
        _orderRepository.Save(order);

        return Result.Success("Order placed successfully.");
    }
}

No bottomless pit of “Domain,” “Infrastructure,” “Application,” and “Presentation” projects. Just a simple feature that does exactly what it says.

Time-Travel Through Architectures

Now for the copypasta history lesson of architectural patterns:

  • Clean Architecture (Uncle Bob, ~2012):
    Isolate business logic from frameworks. Usually misapplied with worthless templates.

  • Onion Architecture (Jeffrey Palermo, 2008):
    Domain at the center, external dependencies on the outside. A CA predecessor.

  • Ports and Adapters / Hexagonal (Alistair Cockburn, 2005):
    Keep core app free from external crap. Define “ports,” connect them with “adapters.”

  • Functional Core, Imperative Shell (long before 2005):
    The functional crowd already separated pure logic from side effects ages ago. Shocking!

The Functional Core, Imperative Shell (FC/IS) Twist

Now, let’s see how you might do it in C# without totally surrendering to OOP dogma:

// The "Functional Core"
public static class OrderLogic
{
    public static bool IsValidOrder(decimal amount) => amount > 0;

    public static Result<Order> CreateOrder(int userId, decimal amount)
    {
        if (!IsValidOrder(amount))
        {
            return Result.Fail<Order>("Invalid order amount");
        }

        var order = new Order
        {
            UserId = userId,
            Amount = amount,
            Status = OrderStatus.Pending
        };

        return Result.Success(order);
    }
}


// The "Imperative Shell"
public class OrderController : Controller
{
    [HttpPost("place-order")]
    public IActionResult PlaceOrder(int userId, decimal amount)
    {
        var result = OrderLogic.CreateOrder(userId, amount);

        if (!result.IsSuccess)
        {
            return BadRequest(result.ErrorMessage);
        }

        // Save the order (side effect)
        SaveOrderToDatabase(result.Value);

        return Ok("Order placed successfully.");
    }

    private void SaveOrderToDatabase(Order order)
    {
        Console.WriteLine($"Saving order: {order}");
        // Actual DB save logic here
    }
}

No DI in the core. Pure functions for business logic. Side effects get shoved to the outer “shell” so your core remains blissfully testable (and untainted by external nonsense).

The Bottom Line

If you can do it with fewer lines, do it. Don’t layer everything like an onion parfait just because you saw a fancy diagram. Don’t abstract everything just because “it might be needed later.” And don’t treat your chosen architecture like the Holy Grail.

For the love of debugging, postpone major decisions—like which database or message broker you’ll use—until you actually know what problem you’re solving. You’ll save yourself from mountains of regret.

Stop Being Dogmatic

All these approaches solve similar problems—they just shine a light on different aspects. Pick what works right now for your context. Don’t be that developer who copy-pastes a “Clean Architecture Template” off GitHub and calls it a day.

So there you have it: CA, VSA, Hexagonal, FC/IS—they’re not as different as you think. They’re tools, not religions. Use them wisely, or watch your codebase burn.