Dev In The Mountain Header
A Developer In The mountains having fun

Pretty, Mostly Working, and Shipped Anyway: What Went Wrong with Software Development

You know the feeling. You install an app with a gorgeous onboarding flow: smooth animations, friendly illustrations, a carefully chosen font. Then, a week later, it quietly loses the note you spent twenty minutes writing. Or an update lands overnight and a feature you used every day now works differently, or not at all. You file a bug report and get an auto-reply. Six months later the bug is still there, but the app has a new icon.

A lot of software today is like that. It looks great and it mostly works. "Mostly" has become an accepted standard, and that should bother us more than it does.


When shipping was hard, software had to be solid

Think about how software used to reach people. It came on floppy disks, then CDs, sometimes in a box on a store shelf. Console games were burned onto cartridges. Firmware was written into ROM chips soldered onto boards. Once it shipped, it was shipped. You couldn't push a fix on Tuesday afternoon.

If a cartridge game had a game-breaking bug, it stayed that way for the life of that copy. If business software corrupted files, the vendor had to mail out new disks, and that cost real money and real reputation. A bad bug in firmware could mean a recall.

That cost shaped how people worked. Teams wrote specs before they wrote code. They tested exhaustively, because the testing was the last chance. They were careful about adding features, since every feature was something more that could break and couldn't be patched. Release dates slipped because "not ready" actually meant something.

To be fair, old software wasn't flawless. It crashed, it had bugs, and plenty of it was clunky to use. Nostalgia makes everything look better than it was. The difference wasn't that developers back then were smarter or more virtuous. The incentives were different. Getting it wrong was expensive, so getting it right was the default, the goal. Now, too often, "good enough" and getting it out the door are the main push. Fixing things slides into the background as more features get added, instead of the base being made more solid.


Continuous deployment changed the incentives

Then the internet made updates nearly free. That's genuinely a good thing: security fixes reach people in hours instead of never, and software can improve over time instead of being frozen on a disc.

But something got lost along the way. "We can fix it later" slowly turned into "we don't have to get it right now."

"Move fast and break things" was a motto at one point. Users became the beta testers without ever agreeing to it. Games launch with day-one patches several gigabytes in size, because the version on the disc was never finished. Operating system updates break drivers. A single bad update from a security vendor can take down machines worldwide in one morning, and the post-mortem reads like a list of the testing steps that a slower release process would have forced.

The ability to update is a safety net. The problem is that a lot of teams now treat the safety net as the floor.


Pretty beats correct

Walk through a modern product team's priorities and you'll find a lot of effort going into things you can see: the design system, the animations, the landing page, the dark mode toggle, the rounded corners.

The things you can't see get much less love:

  • Error handling. What happens when the network drops halfway through saving? Too often, the answer is "the spinner spins forever."
  • Edge cases. Long names, non-English characters, empty states, time zones, leap years.
  • Accessibility. Keyboard navigation, screen readers, contrast. All usually "on the roadmap."
  • Offline and bad-network behavior. Most software is built and tested on fast office Wi-Fi.
  • Performance on ordinary hardware. It runs fine on the developer's new laptop. It crawls on the four-year-old machine most people actually own.

There's a reason for this. Demos and screenshots reward how software looks. Nobody claps in a sprint review because the app correctly handles a disk-full error. The only way to find out whether software is right is to use it for real, for a long time, in messy conditions. By then the team has already been rewarded for shipping it.


The lack of care is a culture problem

It's tempting to blame individual developers, but most developers I've worked alongside want to do good work. The system around them often doesn't let them.

  • Velocity metrics measure how much gets done, not how well it was done.
  • Roadmaps are lists of features. "Fixed 40 long-standing bugs" doesn't make a good slide in the quarterly review.
  • Nobody owns quality. It's everybody's job, which in practice means it's nobody's job.
  • Maintenance doesn't get people promoted. The person who builds the shiny new feature gets the credit. The person who keeps the old one working gets a ticket queue.

And then there's bloat. Chat apps that need more memory than whole operating systems used to. A "simple" web project pulling in dependency trees in the thousands, any one of which can break, go unmaintained, or get compromised. Every layer was added because it was convenient. Very few were added because they were necessary.


AI has accelerated the problem

AI coding assistants are powerful tools, and I use them. But the way they're often used makes these problems worse instead of better.

Code got cheap, so we make more of it. When writing a function used to take an hour, you thought hard about whether you needed it. When it takes ten seconds, you don't. More code means more to read, more to test, more to maintain, and more places for bugs to hide.

AI output looks plausible. That's exactly what makes it dangerous. Generated code is well formatted, nicely named, and often comes with confident comments. It reads like code a careful person wrote, even when it calls an API that doesn't exist, handles the happy path only, or quietly swallows errors.

Review gets thinner. When a pull request is 2,000 lines and came out of a prompt, the reviewer skims it. The author may not fully understand it either. So code gets merged that nobody has actually understood, not the person who asked for it, not the person who approved it.

Patterns get copied without judgment. Models learn from huge amounts of existing code, including all of its bad habits. Ask for a quick solution and you'll get the most common one, which isn't always the right one for your situation.

The result is a growing pile of code nobody wrote on purpose. It works today, mostly. Whether it works tomorrow, or whether anyone can fix it when it doesn't, is another question.


It doesn't have to be this way

None of this is a law of nature. Fast updates, good tooling, and AI assistants can all make software better than it was in the floppy disk era. We just have to choose to use them that way. Some practical places to start:

  • Ship every release as if you couldn't patch it. You can, of course. But pretend you can't, and see how your testing changes.
  • Write the spec or the tests before the code. Decide what "right" means before you decide what "done" looks like.
  • Review AI output like a junior developer's pull request. Read every line. Ask why. If you can't explain it, don't merge it.
  • Count fixing bugs as real work. Put bug fixes on the roadmap and in the release notes. Celebrate them.
  • Keep dependencies small. Every package you add is code you're responsible for but didn't write. Make each one earn its place.
  • Test on slow hardware and bad networks. Throttle your connection. Use an old laptop. Unplug the network halfway through a save.
  • Point AI at the boring parts. This is where AI shines: writing tests, finding edge cases you missed, reviewing code for error handling, keeping docs up to date. Use it to make software more solid, not just to make more software.

There are good examples out there. A lot of the small command-line tools I rely on do one job, start instantly, and keep working year after year. I wrote about some of them in Why I Use So Many CLI and TUI Tools. They aren't pretty in the usual sense. They're built with care, and it shows.

Look at how many of the tools that I, and a lot of other developers, use every day have been around for a long time. grep, sed, make, ssh, vim, curl, rsync, git, SQLite. Some of them are older than most of the people using them. They are still everywhere for a reason.

What stands out is what hasn't changed. grep looks the same today as it did decades ago. Nobody redesigned ssh to follow this year's design trends. There was no big relaunch with a new logo and a reorganized menu. The interface stayed put, so what you learned ten years ago still works today, and so do the scripts you wrote back then.

What has changed is everything underneath. Year after year these tools got faster, handled more edge cases, fixed obscure bugs, closed security holes, and ran on more systems. SQLite is a good example: it's tested so heavily that the test code is far larger than the library itself. That kind of work doesn't show up in a screenshot. Nobody notices the bug that was fixed in a release ten years ago. They just notice that the tool doesn't break.

That's the opposite of the pattern I described earlier. Instead of a fresh coat of paint every year on top of a shaky base, the surface stays the same and the base gets more solid, slowly and steadily. Not by rewriting everything, not by chasing whatever is new, but by small, careful fixes piling up over a long time. That is what being reliable looks like, and it's why people trust these tools with work that actually matters.


Getting it right is a choice

The old developers didn't build solid software because they were better people. They did it because the world forced them to. That pressure is gone now, and we have far better tools than they ever had.

So the only thing left that can make software solid is us. Caring about getting it right, not just getting it out and getting it pretty, is a choice. Make it.

More places to find me
Mental Health
follow me on Mastodon