As long as I have been writing code, people have told me to separate my application into layers. This advice is usually followed by buzzwords like maintainability, testability, separation of concerns, and my personal favorite: decoupling.
The problem is that, for a long time, these words meant nothing to me. When I was younger, I turned on my C64 or PC and just built things. Games, small apps, experiments. Even the biggest monolithic spaghetti code worked just fine.
At the same time, I admired “real” software engineers. They seemed to sit down and just know how to divide their systems into layers and components, building software that could survive for years without collapsing under its own weight.
Unfortunately, many of the best developers I met explained things so terrible, that making spaghetti code sound appealing again.
If you’ve ever felt this way, you’re not alone. You can recognize well-structured software. You enjoy working with it. But two important pieces are missing:
When code is not divided into clear modules or layers, it can still work. But certain problems appear again and again:
All of these are symptoms of the same underlying issue: tight coupling. The real problem is not change itself. The problem is that changes ripple through the system in unpredictable ways.
Look at this function:
def report(amount, price):
print(f"Total: {amount * price} €")
report(10, 4.99)
At first glance, this seems fine. But there is a hidden problem.
The function does two things at once:
This couples your logic directly to the terminal. You cannot reuse or test it without printing. Now consider version 2:
def report(amount, price):
return f"Total: {amount * price} €"
print(report(10, 4.99))
send_email("piet@codersbringchange.com", report(10, 4.99))
This looks like a small change, but it has a big effect:
If you started with version 1 and later realized you need emails, APIs, or tests, you would be forced to change the function from print to return. And even in this tiny example, the rippling is across architectural layers.
Once logic and presentation are separated, the ripple stops. There is a clear interface between them, defined by the function signature.
So to understand decoupling, it helps to think in architectural layers.
Different types of applications use different names, but the underlying structure is very similar. A typical system can be thought of in terms of these layers:
Not every project uses these exact names, and not every system needs all layers.
Even beginners naturally start by separating functionality into components. The problem is that without a clear structure, these modules often remain coupled. They are simply chained together directly, without well-defined boundaries or interfaces between them.
This is where dependency inversion becomes important. It allows modules, components, and even layers to depend on abstractions instead of concrete implementations, reducing direct coupling and making systems easier to change.
To understand what exactly is inverted in dependency inversion, have a look at this video:
What exactly is Inverted 🙃 in the Dependency Inversion Principle?
Written by Loek van den Ouweland on April 27, 2026.