Musings on Software Complexity
June 18, 2020
I’ve spent 22 years in tech. And one thing anyone who’s been in the industry this long will tell you is that the degree of complexity involved in building software for a modern computer, from the bottom up, is truly, mind-blowingly, incomprehensibly, staggering.
Abstractions atop abstractions, dependencies upon dependencies at build and run time, archeological layers of backward compatibility and reasons-for-being, remnants of design trends that came and went, vestigial features never fully developed, unique and bespoke solutions and optimizations, workarounds for hardware, unexpected behaviors long fossilized into an unspoken part of the interface—and all under active development and maintenance by legions of humans, ever-changing.
It’s an awesome testimony to the ability of humans to tame (or at least safely ignore) complexity to a level that fits within working memory; to come up with abstractions that define a problem space which can then be wrangled by a small team.
But once in a while, you look behind the 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 the whole, 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 cost of complexity doesn’t really bother me; software engineers seem to have an instinct to wrestle down complexity using ever-higher levels of abstraction. Companies can—and do—operate profitably at some particular level of the stack, while basically ignoring any higher and lower levels of abstraction.
But the indirect costs of complexity are troubling, including:
- Monoculture - As the cost of pivoting a thick layer of the stack becomes prohibitively expensive, we reuse deep silos of software as-is. We already see this happening with UNIX and Windows.
- Death by a million cuts - Minor flaws show up in all sorts of places, and add up to an unpleasant experience.
- Security and privacy issues - Those minor flaws eventually become vectors of attack by nefarious actors.
- Emergent behavior - Interactions between components1 give rise to unexpected results.
- Accidental single points of failure - Thanks to a complex web of dependencies, it’s not necessarily obvious when one component’s failure might become catastrophic.
- Inefficiency - As abstractions grow ever more divorced from hardware, power consumption and responsiveness can suffer.
I worry about monoculture and security/privacy issues most of all. Monoculture creates a lucrative marketplace for exploits, and may represents a single point of failure (a single vulnerability can bring a lot of software down). Security and privacy exploits have impact far beyond the apps that contain our data.
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. ↩