Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
We’ve been told that clean code and deadlines are opposites.
That if you want to ship fast, you have to write garbage code full of hacks. But if you want to get it right, you need to add boilerplate.
That’s a lie.
Here’s the thing: a lot of the so-called best practices people tell you to follow are a scam. Adding layers, interfaces, and abstractions does not automatically help you ship faster. That is not architecture. A lot of the time, it’s just liability.
YouTube
Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.
The interface tax
Here’s a simple example I call the interface tax.
Say we need to process a payment using Stripe. Of course, because it’s a third party dependency, best practices say we need to abstract it. So we create an IPaymentService with a ChargeAsync method.
Then we create StripePaymentService, which implements that interface and uses the Stripe SDK directly.
Then in our actual usage, we have a ProcessPayment command handler that injects IPaymentService, and that is all wired up through dependency injection so we can call ChargeAsync.
Now back up for a second.
We have this interface. How many implementations do we have? One.
We have one implementation of StripePaymentService.
How many usages of IPaymentService do we have? One.
We have one actual usage, in this command handler. So what is the value exactly?
Do not confuse abstraction with optionality.
If the thought is, “Well, we might switch off Stripe one day,” when is one day? Is it tomorrow? Next week? Never? You do not know.
If your intent was to create a boundary or seam, you already had one. It was the handler.
That means if we ever did need to change something, the rewrite is simple. No interface. No extra implementation. Just use the Stripe SDK directly in the handler.
If we do end up changing payment providers, we change it in that singular place.
If we have to update the SDK and there is a breaking change, we update it in that singular place.
You do not need to add interfaces to feel architecturally responsible.
But what about testing?
Maybe you’re thinking: “But tests. I have other implementations because of tests.”
Testing is not a reason to immediately jump to interfaces like that is the only way you can possibly test.
Most third party libraries I use already give you some way to test. They provide fakes, alternate types, or static helpers. And even if they do not, the point is still just to create isolation.
You can do that in simpler ways, even with something as basic as a virtual method you override in a test.
The bigger issue here is not interfaces. It is that people lose sight of what they were actually trying to get when they applied Clean Architecture, Onion Architecture, Ports and Adapters, or Hexagonal Architecture.
What they were trying to get was isolation. They were trying to control blast radius by managing the direction of dependencies.
Unfortunately, that often turns into adding layers and indirection everywhere, and that just creates pain.
Five layers to do one simple thing
This is such a common complaint, and you’ve probably felt it too:
We have an API trying to pursue Clean Architecture, and I have to go through five layers of code before I get to the actual code to see what it’s doing.
Nine times out of ten, it is doing a simple database query or making a request to a third party API.
Take an example like fulfilling an order.
You inject an IOrderRepository and maybe some IMapper. Often people drift into some generic repository pattern too. You use the repository to get the entity out. Then you map that into some domain model. Sometimes that mapping is hidden in the repository, but it is still there. Then you call Fulfill.
And what is all of this actually doing?
It is just changing the status of a property on the underlying data model.
Then you use the repository to map it back and update it.
Repository. Mapper. Domain model. More mapping.
What are we trying to do here?
We are trying to get data out of the database so we can update it.
That’s it.
So if that is really all the implementation is doing, why not just inject the DbContext directly, find the record, update the status, and save the changes?
If there are no invariants, no rich business rules, no real complexity, what are we gaining from the repository?
What are we gaining from the domain model? Nothing.
The real issue is coupling
It is kind of wild to me that when people talk about Clean Architecture and similar approaches, they rarely talk about coupling directly.
That is really what you are trying to manage. But a lot of people act like coupling itself is bad. It is not.
Without coupling, you have nothing.
Coupling is not bad. Uncontrolled coupling is bad.
In the Stripe example, or in the database example, if you have a single usage of something, you do not have uncontrolled coupling.
If you have a thousand usages of something, now you have uncontrolled coupling. Maybe that is where an interface or some abstraction actually makes sense.
This is not about eliminating coupling.
It is not about decoupling everything.
It is about managing coupling.
Slices create isolation
This is where slices can help, because they create isolation.
In the Stripe example, that payment processing command handler can be a slice. It can define everything around that behavior. Maybe that includes an HTTP API, some application code, and maybe some domain code underneath.
That isolation inside the slice lets you decide how you want to manage coupling and what that slice is coupled to.
One slice might just be an HTTP API with some application code and a simple underlying data model.
Another slice might be more elaborate. Maybe it uses messaging, infrastructure, queues, a bus, application code, and a richer domain model because there is workflow and real business complexity.
Another slice might just be CRUD, and that is fine too.
Not everything needs the exact same layers.
Not everything needs the exact same structure.
This is really about being pragmatic and adding what you need where it actually gives you value.
To me, that is pragmatic architecture.
Control the blast radius
If something changes, it should be localized.
That should sound familiar.
Earlier I talked about the idea that “what if something changes?” and how people use that to justify an interface. But that is just one way to deal with change, and often not a very good one.
This is not about denying dependencies.
It is about controlling where they live.
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.
Sometimes the right move is to ship code that is less than ideal.
Sometimes it is a bit of a dirty hack.
That is fine, as long as it is isolated. As long as it is contained.
Sometimes you need to do things for today so you can have a tomorrow.
Just do not do it in a way that handcuffs you tomorrow.
There are different ways to control blast radius and isolation. It does not always come down to turning everything into abstractions and layers.
Stop paying the tax
A lot of teams are paying an interface tax, a repository tax, and a layering tax without getting any real value in return.
They are solving problems they do not actually have.
The goal is not to look architecturally responsible.
The goal is to build software that is easy to change in the places where change is actually likely.
That means understanding where coupling matters, where isolation matters, and where simplicity is the better tradeoff.
That is architecture.
Not blindly adding layers. Not wrapping every dependency in an interface. Not turning simple code into ceremony.
Just thoughtful tradeoffs, made in the right place.
Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.

