Debug Log

The Interface Tax: Is Clean Architecture a Scam?

April 10, 202614:58Debug Log

This episode critically explores how dogmatic adherence to "Clean Architecture" principles, such as excessive layering and abstraction, can inadvertently hinder development velocity. It introduces concepts like the "Interface Tax" and "Lasagna Code," illustrating how over-engineering for unlikely future changes creates unnecessary complexity and friction for developers. Listeners will gain a critical perspective on common architectural practices and learn to identify when they might be detrimental to project progress.

Key Takeaways

Detailed Report

The long-standing architectural paradigm known as "Clean Architecture," with its emphasis on layers, interfaces, and dependency inversion, is facing scrutiny. While initially embraced as a safeguard against technical debt, critics like Derek Comartin argue that its rigid application can significantly impede development velocity, introducing what he terms the "Interface Tax." This re-evaluation challenges the notion that complex layering is always the "right" way to build software.

The Evolution of Architectural Thinking

The industry's embrace of layered architectures stemmed from past traumas with "Big Balls of Mud" — monolithic N-Tier systems where a single change could cause widespread, unpredictable failures. The introduction of concentric layers, such as Onion Architecture and Hexagonal Architecture by figures like Uncle Bob Martin, Jeffrey Palermo, and Alistair Cockburn, offered a sense of psychological safety and a clear checklist for architectural responsibility. This approach, however, sometimes led to "lasagna code"—still multi-layered, but potentially harder to navigate than the spaghetti it replaced.

Understanding the "Interface Tax"

Comartin's "Interface Tax" highlights the false dichotomy that developers must choose between shipping fast (and creating "garbage") or "doing it right" (and paying a huge boilerplate cost). This "tax" is evident when developers abstract third-party dependencies, like a payment service such as Stripe.

The Stripe Example

A common "best practice" dictates creating an `IPaymentService` interface and a `StripePaymentService` implementation, even when 99% of applications will *never* switch payment providers. This seemingly responsible abstraction incurs a tangible cost:

  • Extra Files: More files to manage and navigate.
  • Increased Cognitive Load: Developers must understand multiple layers for a simple operation.
  • Broken IDE Navigation: Tools like "Go To Definition" often lead to a useless interface, forcing developers to hunt for the actual implementation, often through the Dependency Injection container. This friction slows down development every time it's encountered.

The classic defense, "But what if we switch from Stripe to PayPal?", is often a fantasy. Switching major third-party services is rarely a simple interface swap; it involves fundamental changes to workflows, webhooks, error handling, and even core domain logic. Abstracting for such a scenario is akin to abstracting a car's engine for a potential switch to a jet engine – a solution for a problem that doesn't exist.

Overkill for Simple Operations

If the "Interface Tax" is a misdemeanor, the application of multi-layered architecture to basic Create, Read, Update, Delete (CRUD) operations is considered a "felony." Updating a simple order status, for instance, can involve traversing five rigid layers: API Controller, Use Case/Application Service, Mapper, Domain Model, and Repository. Most of these layers merely pass data through, adding ceremony without significant business logic or protection.

A more pragmatic approach, often met with gasps from traditionalists, is to directly inject the Object-Relational Mapper (ORM) like Entity Framework into the request handler. Entity Framework already functions as a Unit of Work and a repository; wrapping it in custom `IRepository<T>` interfaces often strips away its powerful features, creating an "architectural double-tax."

Debunking the "Testability Shield"

A common defense for extensive interfacing is "But how do I unit test it?!" The belief that interfaces are the *only* way to achieve testability is flawed. Modern development tools have advanced significantly:

  • Fakes and Alternate Types: Many third-party libraries provide built-in testing fakes.
  • Simpler Isolation: Techniques like overriding virtual methods in test classes can achieve isolation.
  • Integration Testing: Tools like Testcontainers allow developers to spin up real, ephemeral databases for integration tests, making excessive mocking of database layers largely obsolete.

Mocking an entire database layer for a simple CRUD unit test often leads to brittle tests that verify implementation details rather than actual behavior, consuming significant time and breaking frequently with minor code changes.

Redefining Coupling

The traditional view often demonizes "tight coupling." However, coupling isn't inherently bad; "Without coupling, you have nothing." The real problem is *uncontrolled* coupling. Having a single usage of a direct dependency (e.g., Stripe SDK or DbContext) within a specific feature handler is acceptable because its "blast radius" is contained to that single file. The danger arises when a thousand direct usages of a dependency are scattered across an entire codebase. Abstractions are truly required when managing such widespread dependencies, not for isolated, single-use cases.

Vertical Slice Architecture: A Pragmatic Alternative

An emerging consensus, championed by figures like Jimmy Bogard and Derek Comartin, is Vertical Slice Architecture (VSA). This approach fundamentally shifts how software is organized.

How VSA Works

Unlike traditional Clean Architecture, which separates code by *technical concern* (UI, business, data), VSA separates code by *business capability* or feature. All components required for a specific feature—API route, validation, database query, mapping—reside together, often in a single folder or even a single file. This drastically reduces cognitive load, as developers don't need to jump between multiple projects or files to understand a feature.

Heterogeneous Architecture

A key strength of VSA is its "heterogeneous architecture." Because slices are loosely coupled to each other, they don't need to adhere to a uniform architectural pattern. One slice might use a simple handler injecting an ORM for a basic CRUD operation, while another, more complex slice might employ a full-blown Domain-Driven Design aggregate with rich invariants and domain events. This allows teams to pay the cost of complexity only where it genuinely exists, promoting pragmatism over dogmatism.

The "Senior Move" and Pragmatism

Moving away from rigid architectural dogma is considered a "senior move." Junior and mid-level engineers often hide behind "best practices" as a defense mechanism. Senior engineers, however, understand that "best practices" are highly contextual and question the value a pattern provides before applying it. This includes embracing "dirty hacks" when their "blast radius" is contained within a single vertical slice, allowing for rapid feature delivery without handcuffing future development of other features.

The AI Imperative

In the age of AI coding assistants, some argue that the "Interface Tax" no longer matters because AI can instantly generate boilerplate. However, this is a dangerous trap. If AI makes producing code cheap, it also makes producing "rat's nest turd piles of coupling" cheap. The true bottleneck in software engineering is no longer writing code, but *reading* and *understanding* it to make safe behavioral changes. Pragmatic, cohesive designs like Vertical Slice Architecture become even *more* crucial, as they make the human task of understanding and maintaining complex systems feasible, even when AI generates much of the underlying code.

Show Notes

Works Referenced

This episode was based on a research prompt rather than a single source URL. List the most relevant resources discovered during research, starting with the most important.

Then list any other articles, papers, reports, projects, companies, tools, standards, or resources that were mentioned in the episode or discovered during research. Format each as a bullet with a bolded name followed by a short description. Where a URL is known, make the name a clickable Markdown link: Name: one-sentence description. Only include items actually discussed or directly relevant to the episode — do not pad with tangentially related links.

  • Why "Clean Code" is Killing Your Velocity by Derek Comartin: A manifesto arguing that dogmatic adherence to "Clean Code" principles, particularly excessive abstraction, can hinder software development velocity.
  • Vertical Slice Architecture by Jimmy Bogard: A blog post introducing and advocating for an architectural style that organizes code by feature rather than by technical layer.
  • Clean Architecture by Robert C. Martin (Uncle Bob): A book and concept proposing a software design philosophy that promotes separation of concerns into layers to achieve independence from frameworks, UI, and databases.
  • The Onion Architecture by Jeffrey Palermo: A foundational article describing an architectural style that places the domain model at the center of the application, with layers of infrastructure and UI depending inwards.
  • Hexagonal Architecture (Ports and Adapters) by Alistair Cockburn: An architectural pattern that isolates the core logic of an application from external concerns through "ports" (interfaces) and "adapters" (implementations).
  • Stripe: A widely used payment processing platform mentioned as an example of a third-party dependency often over-abstracted.
  • PayPal: Another major online payment system, used as a comparison to Stripe to illustrate the complexities of switching payment gateways.
  • Entity Framework Core: A popular object-relational mapper (ORM) for .NET applications, discussed in the context of abstracting ORMs.
  • Moq: A popular mocking library for .NET, mentioned in the context of creating mock objects for testing.
  • Testcontainers: A library that provides lightweight, throwaway instances of databases or other services in Docker containers for integration testing.
  • GitHub Copilot: An AI-powered coding assistant, discussed regarding its impact on boilerplate code generation and cognitive load.

Glossary

  • Clean Architecture: A software design philosophy promoting separation of concerns into layers to achieve independence from frameworks, UI, and databases.
  • N-Tier Architecture: A client-server architecture where presentation, application processing, and data management are logically separate processes, often leading to complex dependencies.
  • Big Ball of Mud: A pejorative term for a software system that lacks a discernible architecture, often characterized by entangled dependencies and spaghetti code.
  • Dependency Inversion: A principle of object-oriented design stating that high-level modules should not depend on low-level modules; both should depend on abstractions (interfaces).
  • Onion Architecture: An architectural style that places the domain model at the center of the application, with layers of infrastructure and UI depending inwards.
  • Hexagonal Architecture (Ports and Adapters): An architectural pattern that isolates the core logic of an application from external concerns (like UI or databases) through "ports" (interfaces) and "adapters" (implementations).
  • Interface Tax: A term describing the unnecessary overhead (extra files, cognitive load, broken IDE navigation) incurred by creating interfaces and abstractions for dependencies that are unlikely to change or only have a single implementation.
  • Boilerplate: Sections of code that are repeated in multiple places with little or no alteration, often required to satisfy architectural patterns or frameworks.
  • DI Container (Dependency Injection Container): A software framework that manages the creation and lifecycle of objects and their dependencies, injecting them where needed.
  • CRUD: An acronym for Create, Read, Update, and Delete, representing the four basic functions of persistent storage operations.
  • ORM (Object-Relational Mapper): A programming technique for converting data between incompatible type systems using object-oriented programming languages.
  • DbContext: In Entity Framework, a class that represents a session with the database and can be used to query and save instances of your entities to a database.
  • Unit of Work: A design pattern that maintains a list of objects affected by a business transaction and coordinates the writing out of changes and resolution of concurrency problems.
  • Repository: A design pattern that mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects.
  • Moq: A popular mocking library for .NET, used to create mock objects for testing purposes.
  • Testcontainers: A library that provides lightweight, throwaway instances of databases, message brokers, or other services in Docker containers for integration tests.
  • Coupling: The degree to which software modules depend on each other; high coupling means modules are highly interdependent, while low coupling means they are more independent.
  • Vertical Slice Architecture (VSA): An architectural style that organizes code by feature or business capability, where all components related to a specific feature (UI, business logic, data access) are grouped together.
  • High Cohesion: A software design principle where the elements within a module belong together and work towards a single, well-defined purpose.
  • Low Coupling: A software design principle where modules are designed to be as independent as possible from each other.
  • Heterogeneous Architecture: An architectural approach where different features or "slices" of an application can employ different architectural patterns or levels of complexity as appropriate for their specific needs.
  • Domain-Driven Design (DDD): An approach to software development that centers on modeling the domain (the problem space) and using that model to guide the design of the software.
  • Aggregate: In Domain-Driven Design, a cluster of domain objects that can be treated as a single unit for data changes, with a root entity that controls access to the others.
  • Domain Events: In Domain-Driven Design, something that happened in the domain that you want other parts of the same domain or other domains to be aware of.
  • AI Coding Assistant (e.g., GitHub Copilot): Tools that use artificial intelligence to help developers write code faster by suggesting code snippets, completing lines, or generating entire functions.

Full Transcript

HostOkay, so picture this: you've been grinding away, trying to implement the perfect "Clean Architecture" in your latest project. Layers upon layers, interfaces for everything, dependency inversion... you know the drill. You feel like you're doing everything "right."
ExpertRight, you've got that smug architect feeling, like you're building a fortress against future technical debt. You're thinking, "This is future-proof, baby!"
HostExactly! And then someone like Derek Comartin comes along, drops a manifesto called "Why 'Clean Code' is Killing Your Velocity," and basically calls your entire sacred cow a scam.
ExpertAnd suddenly, that fortress feels less like a bastion of good design and more like a gilded cage, locking you away from actually *shipping* anything. It’s a gut punch, but frankly, it’s a gut punch a lot of us needed.
HostI mean, we've all been there, right? The industry spent years telling us that layering was the answer to everything. We were traumatized by those N-Tier architectures that ended up as "Big Balls of Mud" where one change in the database broke the UI.
ExpertOh, the trauma was real. I remember one project where we tried to update a stored procedure, and suddenly a completely unrelated part of the app crashed because of some implicit dependency we hadn't even documented. It was like pulling a thread and watching the whole sweater unravel.
HostSo, naturally, when guys like Uncle Bob Martin, Jeffrey Palermo, and Alistair Cockburn came along with their concentric layers, Onion Architecture, Hexagonal Architecture—it felt like salvation. It gave us a checklist. You put an interface here, a mapper there, a repository for *that*... and boom, instant architectural responsibility.
ExpertThat "psychological safety" is key, isn't it? It felt good to *feel* like you were doing things the right way. It became almost a dogma. But as Comartin points out, that safety is often an illusion. We traded spaghetti code for what I like to call "lasagna code"—still many layers, just neatly stacked. And sometimes, even harder to navigate.
HostLasagna code, I love that. And that brings us right to the core of his argument: what he calls the "Interface Tax." This idea that developers are conditioned to believe that you either ship fast and create garbage, or you "do it right" and pay this huge boilerplate tax. He argues that's a lie.
ExpertIt's a massive lie! And the perfect example he gives is integrating something like Stripe. We all know the "best practice," right? You need to abstract that third-party dependency. So, you create an `IPaymentService`, a `StripePaymentService` implementation, and then your `ProcessPayment.Handler` injects the interface.
HostExactly. You're following the rules, you're being "architecturally responsible." But here's the kicker: for 99% of applications, you are *never* going to have another payment service. You have one implementation, and you've paid this tax for a scenario that will literally never happen.
ExpertAnd what is that tax? It's extra files, increased cognitive load, and the absolute worst offender: breaking IDE navigation. You hit "Go To Definition" and it takes you to a useless interface that tells you absolutely nothing about the actual code that's running! You then have to hunt down the implementation, often diving into your DI container setup just to figure out what's going on. That's real, tangible friction for developers.
HostOh, the "Go To Definition" pain. You ever hit F12 on a method to see what it does, and Visual Studio drops you into an interface with zero implementation? And then you have to search the DI container to figure out what's actually running? That's the Interface Tax right there. It slows you down every single time you encounter it.
ExpertAnd the classic defense is always, "But what if we switch from Stripe to PayPal?" And as seasoned engineers, we know that's just... a fantasy. Switching payment gateways is never as simple as swapping an `IPaymentService` implementation. PayPal and Stripe have fundamentally different workflows, webhook structures, error handling, not to mention the domain logic changes. Your domain *will* have to change.
HostAbsolutely. It's like saying, "We need to abstract our car's engine, just in case we switch from gasoline to a jet engine." Sure, they both make the car go, but the interfaces, the fuel, the whole system is completely different. You're abstracting a problem you don't actually have.
ExpertAnd if the Interface Tax is a misdemeanor, then what they do to simple CRUD operations in the name of Clean Architecture... that's a felony.
HostOh, tell me about it. The multi-layered mirage for a basic database update. I've seen it. Imagine you just need to update an order status from `Pending` to `Fulfilled`. In dogmatic Clean Architecture, that's five rigid layers you have to traverse.
ExpertFive layers! You start with the API Controller receiving the HTTP request. Then it goes to a Use Case or Application Service to coordinate the workflow. Then a Mapper maps the database entity to a Domain Model. The Domain Model *finally* has the `Fulfill()` method, which just flips a boolean. And then a Repository maps it back to a database entity and saves it. What are we even doing here?
HostIt's like making a seven-layer dip just to eat a single tortilla chip. The amount of effort for such a simple operation is insane. Most of these layers are just... passing data through. They're not adding any real business logic or protection.
ExpertNine times out of ten, they're just glorified data pipelines. And Comartin's pragmatic heresy, which makes enterprise architects gasp, is so refreshing: just inject the ORM directly.
HostOh, I can hear the gasps now! "Inject the DbContext directly into the request handler?!" The horror! But seriously, if there are no rich business invariants to protect, if it's just flipping a boolean, why add all that ceremony? Find the record, update the status, save changes. Done.
ExpertExactly. Entity Framework *is* a Unit of Work. It *is* a repository. When you write `IRepository<User>`, you're just putting a cheap plastic hubcap over a Ferrari. You're abstracting an abstraction that already exists and works perfectly well, and in doing so, you're stripping away all the powerful features of the underlying ORM. It's an architectural double-tax.
HostYou know, whenever you challenge Clean Architecture, the immediate, reflexive defense is always, "But how do I unit test it?!" Like, without interfaces, our code just becomes this untestable blob.
ExpertThat's the "testability shield." And it's fundamentally flawed. Developers have been brainwashed into believing that interfaces are the *only* way to test. But it's just not true anymore, if it ever was.
HostRight. Modern tools have come so far. Most third-party libraries provide fakes or alternate types for testing. And you can achieve isolation in much simpler ways, like just overriding a virtual method in a test class if you really need to.
ExpertAnd the industry is finally waking up to the fact that mocking an entire database layer just to unit test a CRUD operation is a monumental waste of time. It leads to brittle tests that verify *how* you wrote the code, not *what* it actually does. If your test file has 45 lines of `Moq.Setup().Returns()`, you aren't testing your code. You're testing your ability to write mocks.
HostIt's so true. And then you change one thing in the implementation, and half your tests break, even though the *behavior* of the system hasn't changed at all. That's a huge drag on velocity. Tools like Testcontainers have made integration testing fast and reliable, letting you spin up a real, ephemeral database for your tests. That makes excessive interface-mocking completely obsolete for many scenarios.
ExpertExactly. You're testing against something much closer to reality, which gives you more confidence. And this all ties into a deeper redefinition of "coupling." We've been told that all coupling is evil.
HostYeah, "tight coupling" is always the boogeyman.
ExpertBut Comartin argues that coupling isn't inherently bad. "Without coupling, you have nothing," he says. And he's right! Your code has to do *something*. The problem isn't coupling; it's *uncontrolled* coupling.
HostThat's a profound distinction. So, having one usage of a direct dependency, like the Stripe SDK or a DbContext, in a specific feature handler is perfectly fine. The blast radius is contained to that single file.
ExpertExactly. If that one file breaks, it's just that one file. But having a thousand direct usages of a dependency scattered across your entire codebase? *That's* dangerous. *That's* when an interface or an abstraction is actually required. The industry's mistake was applying the solution for a 1,000-usage problem to a 1-usage reality. We over-abstracted for the wrong problems.
HostSo, if Clean Architecture is actually slowing us down, what's the alternative? We can't just go back to spaghetti code, can we?
ExpertAbsolutely not. The emerging consensus, championed by people like Jimmy Bogard and Comartin, is Vertical Slice Architecture, or VSA. And it completely flips the way we organize our software.
Host"Slicing the cake," right? I love that analogy. Clean Architecture asks you to eat all the frosting, then all the sponge, then all the filling. VSA cuts you a whole slice from top to bottom.
ExpertExactly! Traditional Clean Architecture separates code by *technical concern*: UI layer, business layer, data layer. VSA separates code by *business capability* or feature. So, all the code required for a specific feature—the API route, the validation, the database query, the mapping—it all lives together, often in a single folder, sometimes even a single file.
HostThat drastically reduces cognitive load. You're not jumping between five different projects or ten different files to understand how a "Cancel Order" button works. Everything you need is right there.
ExpertIt's high cohesion, low coupling at its best. And the real superpower of VSA is what they call "heterogeneous architecture." Because slices don't couple to each other, they don't have to share the same architectural patterns.
HostWait, so you're saying I could have one slice that's just a handler injecting a DbContext for a simple CRUD operation... and another slice right next to it that's using a full-blown Domain-Driven Design aggregate with rich invariants and domain events?
ExpertPrecisely! You only pay the cost of complexity where the complexity *actually exists*. You don't force a "one size fits all" architecture across the entire application. That's a huge win for pragmatism. It's not about being dogmatic; it's about using the right tool for the right job, within a contained boundary.
HostThis all sounds like a massive mindset shift, moving away from dogma. Comartin calls it "the senior move."
ExpertHe does. "One of the most senior moves you can make is to stop following patterns blindly and start understanding the value they are supposed to provide. Then ask whether you actually need that value." Junior and mid-level engineers often hide behind "best practices" because it provides a defense mechanism in code reviews. But senior engineers recognize that "best practices" are highly contextual.
HostThat's spot on. It's the difference between knowing the rules and knowing *when to break them*. And it ties into another controversial idea: embracing the "dirty hack," as long as its blast radius is contained.
ExpertThis is liberating! In a vertically sliced system, if you ship a messy, procedural, 300-line function that gets a critical feature out the door, and that code is isolated to a single vertical slice that no other feature depends on, that's a win. If it needs refactoring later, the refactoring is entirely localized. It doesn't handcuff future development of other features.
HostSo, it's not about never writing "dirty" code; it's about ensuring that if you do, it can't infect the rest of your system. You've contained the blast radius.
ExpertExactly. It's pragmatic over pure. And let's not forget the AI imperative here, especially in 2026. Some developers argue that since AI coding assistants can generate boilerplate instantly, the "Interface Tax" no longer matters.
HostYeah, "My Copilot can write the five layers of Clean Architecture in three seconds, so who cares?" I've heard that.
ExpertBut Comartin warns that's a dangerous trap. If AI makes producing code cheap, what else is going to be cheap? Creating a rat's nest turd pile of coupling! The bottleneck in software engineering isn't writing code anymore; it's *reading* and *understanding* code to make safe behavioral changes.
HostThat's a crucial point. If AI generates thousands of lines of useless interfaces and mappers, the human engineer still has to read and maintain that cognitive load. AI might write it, but *we* still have to debug it at 2 AM.
ExpertSo, pragmatic, cohesive design like Vertical Slices matters *more* in the age of AI, not less. It makes the human's job of understanding and maintaining the system actually feasible.
HostSo, to wrap this up, if there are three things listeners should really take away from this, what would they be?
ExpertFirst, that "Interface Tax" is absolutely real and it's expensive. Wrapping single-use dependencies in custom interfaces, like the Stripe SDK example, adds liability, breaks code navigation, and gives you an illusion of decoupling without actual flexibility.
HostYeah, don't pay a tax for a problem you don't have. Second, multi-layered architecture is often massive overkill. Forcing simple database updates through all those layers—APIs, Use Cases, Mappers, Domain Models, Repositories—creates an illusion of clean code at the expense of productivity.
ExpertAnd third, coupling isn't inherently evil; it's *unmanaged* coupling that's the problem. The goal isn't to decouple everything; it's to isolate the blast radius of changes. Vertical Slice Architecture achieves this by managing coupling at the feature level, allowing teams to mix simple CRUD and complex DDD within the same application. It's about being smart, not dogmatic.
HostI think that's a mic drop moment. So, for our listeners, how often have you fallen into the Clean Architecture trap, layering for the sake of layering? And what's one "senior move" you've made recently by *not* blindly following a pattern?