I still remember the sound. That rising, recorded voice announcing you’ve got mail, after a modem had spent thirty seconds screeching its way onto the internet. By modern standards, the whole experience was awful. It was slow, noisy, unreliable, and completely incapable of doing half the things we now take for granted. But there was something good hidden inside those limitations: the software usually did one thing, and then stopped. You gave it an email address, it gave you your mail, and that was more or less the entire relationship.
I think about that more often than I probably should. I don’t want dial-up back, but those systems were forced into a kind of restraint that we’ve largely lost. They couldn’t collect endless telemetry in the background (usage data sent back to whoever made the software) because there wasn’t enough bandwidth, and they couldn’t quietly build detailed profiles of everything you did because storage was expensive and there often wasn’t a good reason to keep that data anyway. That was a hard limit rather than better software engineering, but hard limits have a useful habit of forcing people to decide what actually matters.
When a machine can only do a handful of things, you have to choose which things are worth doing. You don’t get to throw in an extra service, integration, analytics stream, background worker, or feature because it might be useful some day. There isn’t room. Modern software has the opposite problem, because there’s almost always room. Storage and compute are cheap, bandwidth is everywhere, and adding another dependency (someone else’s code that your project now relies on) is often little more than a line in a package file.
Because we can do all of those things, we usually do. Capturing an extra event or spinning up a new service is technically easy enough that the question quickly shifts from “should we?” to “why not?” That shift is responsible for a lot of the software that now feels bloated and fragile, and sometimes slightly hostile to the person using it.
Email is the easiest example, because I still use it every day. It used to feel personal. Someone wrote something to you and sent it. Now the average inbox is stuffed with newsletters nobody remembers asking for, account notifications, login codes, receipts, marketing sequences, abandoned basket reminders, automated status updates, and dozens of other messages generated by systems rather than people.
Most of that isn’t malicious. Nobody sat down and decided to ruin email. Sending it became cheap and easy, so every product eventually found a reason to send more. Once the capability existed, restraint became a choice rather than a limitation, and in most organisations the incentives favour adding one more automated message over removing one.
The same pattern shows up everywhere in software. A platform gains a new capability, so somebody builds a feature around it. The feature looks harmless enough, and maybe a few customers even ask for it. Six months later it has configuration, permissions, documentation, metrics, edge cases, support tickets, database tables, and another external service sitting behind it. Now it can’t easily be removed, because something somewhere depends on it.
Repeat that process for a few years and you end up with a system doing twenty things that are vaguely related to the one thing it originally existed to do. In my experience, that’s how software becomes bloated. It rarely comes from one spectacularly bad decision. It comes from hundreds of individually reasonable ones, each adding a little more surface area until nobody can comfortably hold the whole system in their head any more.
One of the strongest opinions I hold, after two decades of work where getting things wrong has real consequences, is that complexity should have to justify itself. I’m not arguing for primitive software or against useful features, only that every additional capability should be treated as a liability before it’s treated as an asset.
Every feature has a permanent cost. You have to maintain it, secure it, test it, document it, and understand how it interacts with everything else. You have to update it when the platform underneath it changes, and somebody has to remember why it exists three years from now when the original developer has moved on. Every new piece also creates another place for something unexpected to happen, so the cost carries on long after the day you shipped it.
A lot of engineering effort gets spent trying to make complicated systems safe, reliable, and understandable after we’ve already made them complicated. We’d be better off spending more of that effort avoiding the complexity in the first place. The most reliable systems I’ve worked on weren’t reliable because somebody had anticipated every possible failure. They were reliable because there weren’t that many things capable of failing.
Working on those systems changed the question I ask before I start building something. I’m much less interested in what a system could do than in the least it needs to do while still solving the problem properly. That usually leads to a smaller system with fewer moving parts and, more importantly, fewer assumptions hiding inside it.
Doing less often delivers more value, because the important part of the system gets the attention it deserves instead of competing with a dozen peripheral features. It also makes life much easier for the engineer who has to come back later and figure out why something works the way it does.
Small systems are easier to understand. Fewer components means fewer interactions to reason about, fewer dependencies means fewer surprises when a package you rely on changes, less data means less to protect, and fewer endpoints means a smaller attack surface (fewer doors for an attacker to try). There’s a security lesson buried in there too: the feature you never built is the feature you never have to patch, monitor, or accidentally expose.
That sounds obvious when written down, but modern software development constantly pushes us in the opposite direction. Platforms advertise capabilities, frameworks encourage integrations, product roadmaps grow faster than they shrink, and nobody gets particularly excited about the feature you decided not to build. Restraint doesn’t demo very well, and it doesn’t produce many impressive roadmap slides.
Users still feel it, though. They feel it when an application opens quickly and does exactly what they expected, or when the settings page has ten options instead of eighty. They feel it when an upgrade doesn’t break something unrelated, and when they can understand what a product does without reading a manual. Good software often feels simple because somebody worked very hard to keep unnecessary things out of it.
Dial-up gave us that discipline by accident. The hardware forced the decision because machines couldn’t carry much more, and we don’t have that excuse any more. Saying no to something you could build takes much more discipline than never having the option in the first place. Now that the limits are mostly gone, restraint has to come from the people building the software.
I’ve come to see that discipline as part of the job. The platform will allow far more than the system needs, so the work is understanding why the system exists, building that part exceptionally well, and being suspicious of everything that tries to attach itself to the edges. The old system said “You’ve got mail”, gave you the thing you came for, and then got out of the way. There are worse standards to build software by.