In the last post, we talked about complexity in broad strokes — the idea that some of it is necessary, and some of it isn’t, and that software design is largely about eliminating what isn’t and controlling what is. But complexity is worth looking at more closely, because understanding what it actually is — and what it does to a system — is what makes the fight against it feel worth having.
John Ousterhout, in A Philosophy of Software Design, gives a definition that has stuck with me: complexity is anything related to the structure of a software system that makes it hard to understand and modify. That’s it. Not a measure of how many lines of code you have, or how sophisticated the algorithm is, or how many services are running. Complexity is about how hard it is to work with. And that framing matters, because it makes complexity something you can feel — and something you can fight.
So why does it matter if a system is hard to understand and modify? That might sound obvious, but it’s because software never stops growing. Every new feature, every bug fix, every improvement is a change. If your system is hard to change, growth becomes expensive. And expensive, in the real world, means slow, risky, and eventually impossible.
There’s another dimension to this. Modern software systems are far too large to hold in your head all at once. We are not talking about a fizz-buzz or a library management system with three requirements — we are talking about millions and millions of lines of code. You’ll never see or understand the whole thing. This is normal and unavoidable. But it means that every time you make a change, you’re making it with incomplete information. You’re reasoning about a part of the system while the rest of it sits just outside your view. The less complex the system, the safer that is. The more complex it is, the more likely that the part you can’t see is about to bite you. The goal is to be able to work on a part without necessarily understanding the whole — as safely and confidently as possible.
This is easier said than done, especially as a system grows. And growth is where complexity becomes most dangerous, because software is built incrementally — layer by layer, decision by decision — and early decisions have a way of outlasting their welcome.
Think about building a house on a cracked foundation. The crack might seem minor at first — easy to overlook, easy to work around. But every wall you build on top of it inherits the problem. The floors sit unevenly. The doors don’t close right. Every room added makes the underlying issue harder to address, because now there’s more built on top of it. The foundation was the wrong place to cut corners, and by the time that’s obvious, fixing it means tearing apart everything above it.
Software works the same way. Early decisions become the foundation on which everything else is built. When those decisions carry unnecessary complexity, every layer added on top of them inherits it. What starts as a manageable system becomes something you’re afraid to touch.
Think about this every time a new feature or requirement is introduced: it will either fit naturally into your current design, or you’ll try to force it in — which happens more than we like to admit. Maybe it doesn’t seem like a big deal, or you’re already running late and don’t have time to reconsider the design. So you make it fit. You stretch the model to hold something it was never meant to. This is where a lot of the damage happens: the scope and purpose of the system grows, but the underlying design doesn’t grow with it. The complexity accumulates quietly, and the system becomes harder to work with in ways that are difficult to trace back to any single decision.
But it’s worth saying that the goal isn’t to redesign every time something new comes in — that would be exhausting and impractical. The goal is to design with change in mind from the start, so that new things can be accommodated without breaking what’s already there. But when a change reveals that the current design genuinely can’t hold it, the right response isn’t just to bolt it on and move forward. It’s to step back, revisit the design, and bring the mental model back into alignment with what the system has become. Bolting on without rethinking is exactly how complexity accumulates quietly — one small compromise at a time.
This is what complexity actually looks like in practice — not as an abstract property of the system, but as something you feel every time you sit down to work. Here are the ways it tends to show up.
You go to make what seems like a simple change. One thing. Maybe a new rule in the business logic or a label in the UI. And then you discover it’s not one thing at all. It’s the same change in five different places, or a change in one place that breaks something in another, or a change that requires you to update something you didn’t even know was related. What should have taken an hour takes a day. And the risk of getting it wrong is higher than it should be, because the system isn’t telling you everything you need to know. At this point, the system isn’t just hard to work with — it’s genuinely risky. Every change is a gamble.
What makes all of this especially difficult is that complexity doesn’t stay where you put it. Left alone, it grows. The system drifts, slowly and steadily, toward disorder. Keeping a system simple, though, isn’t a one-time decision. It’s ongoing work. It requires deliberate effort, on every change, to resist the pull toward more complexity.
Trust me — you will spend far more of your career reading and building on existing code than writing it from scratch. This is why complexity is worth taking seriously — not just as an abstract concept, but as something that will shape your day-to-day experience as a developer more than almost anything else. The good news is that once you can see it clearly, you can start to fight it. And that’s exactly what software design gives you the tools to do.
Top comments (0)