It occurred to me recently, while neck-deep in legacy code, that every commit made to a codebase almost by definition increases entropy. This is the Second Law of Software Thermodynamics: left unchecked, your system inevitably trends toward disorder.
We call this accrued disorder “technical debt,” but unlike financial debt, nobody signs paperwork when it’s incurred. It happens silently, commit by commit, even with the best intentions.
The Death by 1000 Paper Cuts
The most insidious aspect of code entropy is its compounding nature. No single change is catastrophic. Each individual shortcut or imperfect abstraction seems harmless in isolation:
“I’ll just add this one special case.”
“Let’s copy this logic here for now; we’ll refactor later.”
“This service already knows about that domain concept, so let’s extend it.”
Each of these micro-decisions slightly increases the system’s disorder. But the effects compound. As entropy rises, each subsequent change becomes more difficult:
- Initial complexity makes the codebase harder to understand
- Misunderstanding leads to suboptimal implementation choices
- Suboptimal implementations create more complexity
- Repeat at an accelerating pace
This compounding effect explains why technical debt never accumulates linearly. It’s exponential. A codebase with twice the entropy isn’t twice as hard to maintain—it might be ten times as hard.
Entropy Containers
If every change increases entropy, are we doomed? Not necessarily. The key is to contain entropy within well-defined boundaries.
Well-designed APIs serve as entropy containers. They provide stable interfaces while hiding implementation details. When domain logic is properly encapsulated, new callers don’t actually increase the marginal entropy of that subsystem.
This is why breaking linkages between different parts of code is so powerful. Loose coupling means entropy in one area doesn’t leak into others.
The best code isn’t necessarily the cleverest or most concise—it’s code that contains its complexity effectively, preventing it from contaminating the rest of the system.
A Case Study in Entropy
At work, I recently tackled our 2FA code-sending logic—a perfect example of entropy run wild. This critical functionality existed in at least three different controllers, each with slight variations:
- Some had rate limiting, others didn’t
- Some implemented feature flags for different sending methods
- Some included side effects: redirecting users, reporting failures, updating cookies
Over time, these implementations had diverged significantly. Fixing a bug in one place meant it still existed in the others. Adding a feature required changes in multiple locations. Documentation was perpetually out of date.
The source of this entropy was clear: the lack of proper encapsulation. The solution was equally clear: wrap this logic in a dedicated service with a clean API.
By creating a microservice that handles all 2FA code-sending, we:
- Established a single source of truth for this functionality
- Centralized rate limiting and feature flag logic
- Made behavior consistent across all entry points
- Created an audit log for debugging and analytics
- Provided a clear interface for future extensions
The surrounding code became simpler, and the 2FA logic itself became more maintainable despite its inherent complexity. We didn’t eliminate complexity—we contained it.
Entropy Management as Engineering Discipline
Managing code entropy isn’t a one-time effort. It’s a discipline that needs to be practiced with every commit.
The most effective teams I’ve worked with follow a simple principle: try to leave the codebase slightly better than you found it. This doesn’t mean rewriting systems alongside every feature—that creates more entropy, not less. It means making small, targeted improvements to the areas you touch.
Well-defined boundaries between subsystems are your strongest defense against entropy spread. When responsibility is clearly allocated and interfaces are clean, complexity has natural barriers that prevent it from metastasizing throughout your codebase.
Ultimately, the goal isn’t zero entropy—that’s impossible. The goal is to organize entropy into manageable pockets with clear boundaries, so changes in one area don’t cascade through the entire system.
Next time you’re about to make a “small exception” to an abstraction or duplicate logic “just this once,” remember: you’re not just adding one more piece of tech debt. You’re increasing the system’s entropy, making every future change exponentially more difficult.
Contain that entropy. Your future self will thank you.