Old Software Was Fast Because It Had No Choice | Yusuf Aytas
A few weeks ago we were discussing a Java component that starts a Spark cluster. Its job is mostly coordination. It starts the machinery, passes configuration around, waits for the right signals, and gets out of the way.
My first reaction was simple: it should need one CPU and maybe 2 GB of memory if we are being generous. It is a launcher. Even saying 2 GB felt strange, because this was production, not a toy running on my laptop. But that reaction is exactly the problem. Somewhere along the way, we started treating small numbers as unserious just because the system is important. Production should make us more careful with resources, not less.
I think we all know how that happens. If you’ve been in this business for a while, you have done versions of it yourself. A pipeline fails once, and someone bumps the memory. A service gets squeezed a bit, and someone raises the number of CPUs. A rollout goes poorly, and the next engineer adds a massive buffer because nobody wants to be paged again for trying to be clever with resources. The bigger number works, the system feels resilient, and we move on.
Six months later, our original cause has vanished from everyone’s memory. It becomes the de facto setting for the service. Nobody thinks of it as a temporary fix anymore. We read it as a hard requirement. The patch has hardened into fact. And nobody wants to challenge that because it works.
Here is the paradox. The JVM has absorbed decades of optimization, garbage collectors have gotten dramatically better, CPUs are way faster, and cloud computing is trivial to provision. On paper we should be swimming in the gains. Instead we keep spending them, proving Niklaus Wirth's 1995 law: "software is getting slower more rapidly than hardware becomes faster."
Just In Case Engineering
Where the Progress Went
The gains did not disappear. We spent them on larger runtimes, deeper dependency graphs, heavier containers, more telemetry, wider safety margins, and platform defaults that make every service look the same. When a higher-level abstraction makes coding more efficient, we do not write less code or build simpler systems. We spend the efficiency on more complexity, more integration, more moving parts.
When an engineer inflates a container's memory limit just in case, the JVM's ergonomics read that headroom as permission. Default heap sizing grows to a fraction of whatever it sees, garbage collection gets lazier because there is slack to burn, and the runtime settles comfortably into the larger footprint it was handed. The software does not get hungrier because the work grew. It expands to fill the runway it was given.
Now, some of this weight is legitimate. I want to avoid the trap of technical nostalgia. We have to separate unavoidable complexity from avoidable waste. Modern software runs in a hostile, globally networked environment. Some weight is real: security, accessibility, distributed systems, compliance, observability, and global scale. A modern system carries work old software never had to carry. Our mistake is using that true statement to defend every bad default that came along for the ride.
When you look at what’s avoidable, you can see it in bloated dependency trees, where large portions of imported dependencies are rarely, barely, or never exercised at runtime. Every layer of the modern stack has an appetite. Logging, tracing, the platform SDK, and the base image want their own share. None of them looks outrageous alone, but together they make a small thing stop feeling small. Bloat arrives as a long sequence of rational and local decisions.
The Machine Used to Say No
Old software was not fast because engineers were morally superior. It was fast because the machine said no. In the 2000s, it was normal to run a web service on a machine with a few gigabytes of memory and one or two CPUs. The box was small, so the work had to be shaped properly. You had to prioritize, tune, and understand what the service was actually doing. Constraint made people care.
Modern infrastructure is far more forgiving. We happily add buffers, retries, and standard platform layers, each one just in case. Forgiveness lets us build larger systems, but it also lets waste survive long enough to look normal. A bigger instance type can hide confusion. A bigger heap can hide a leak. A standard platform can hide the fact that a tiny coordinator inherits the appetite of a much larger service because 2 GB looks like a joke.
On the flip side, a service utilizing a microscopic 10% of its allocated 64 GB shows up as a beautiful, calming green on a monitoring chart. It signals health to the reliability engineering team, effectively masking a potential optimization failure as a triumph of operational safety. We have built exhaustive alerts for system saturation, but we rarely build alerts for structural emptiness.
The distance between the engineer and the machine collapsed....