Musings on Software Complexity
June 18, 2020
I’ve spent 22 years in tech, and the complexity involved in building software for a modern computer from the bottom up is still truly, mind-blowingly, incomprehensibly, staggering to me.
Abstractions atop abstractions, dependencies upon dependencies at build and run time, archeological layers of backward compatibility and reasons-for-being, evidence of design trends that came and went, vestigial features that were never fully fleshed out, unique, bespoke solutions and optimizations, workarounds for hardware, unexpected behavior that fossilized into an unspoken part of the interface—and all under active development and maintenance by legions of humans, ever-changing. It’s a towering testimony to the ability of humans to tame (or at least safely ignore) complexity to a level that fits within their working memory; to come up with abstractions and metrics that define a problem space that can be managed by a small team.
But once in a while, you look behind the thin veil and see the billions of tiny gears meshing to create a working machine, and wonder how it all works; and yet, it mostly does. No one can comprehend all the complexity, yet somehow, everything comes together as a useful tool.
I’m not sure whether this is a reason to celebrate or mourn, but it is something marvelous to behold.
The direct costs of complexity doesn’t really bother me all that much because software engineers appear to have an instinct to tamp down and contain complexity using ever-higher levels of abstraction. Organizations can (and do) successfully operate at a particular stratum of the stack while ignoring higher and lower levels of abstraction.
However, the indirect costs are troubling, including:
- Monoculture, as the cost of pivoting a thick layer of the stack becomes prohibitively expensive (we already see this with UNIX and Windows silos)
- Death by a million cuts as minor flaws show up everywhere
- Constant security, availability, and privacy issues as those flaws become vectors for adversaries
- Emergent behavior due to unforeseen second-order interactions between components1
- Accidental creation of single points of failure due to a complex web of dependency, such as a vital tool, resource, or trust chain
- Inefficiency and power consumption as abstractions grow ever more divorced from hardware
Of all these things, I worry about security/privacy and monoculture most of all. Monoculture creates a large addressable market for exploits and creates a systematic single point of failure (i.e. dependency on a single vulnerable architecture), making it profitable for adversaries to pool their resources to compromise a user’s data or privacy.
As software becomes more complex, it becomes ever more likely that one engineer working on one component will have contradictory design goals with an engineer working on another. These conflicting assumptions can cause puzzling interactions to emerge when the components are put together. ↩