We Broke Software
How we learned to ship shit and call it delivery
There’s a specific kind of frustration that only developers know. It’s not the frustration of a bug you can’t track down, or a stakeholder who keeps moving the goalposts. It’s the frustration of shipping something you know isn’t right.
2014 - Enterprise CMS rebuild. The whole promise of the project was better performance, better UX — and buried in that promise was search. Search was the thing that mattered most to the client. Search was the thing that was supposed to actually get fixed.
It didn’t get fixed. It got band-aided. And we shipped it anyway.
I led that project. That’s on me. And I’ll be honest, it wasn’t relief when we finally shipped it. It was frustration. Because delivery is always expected of us. But there’s a difference between delivering and delivering shit. Somewhere along the way, we stopped feeling that distinction as sharply as we should.
What I didn’t have a name for back then, we have a name for now: enshittification.
The Pattern Has a Name
Enshittification, coined by writer Cory Doctorow, describes the gradual decay of digital products. It usually gets applied to platforms: how they start by serving users, then pivot to extracting value from them. But the same pattern plays out in how we build software, sprint by sprint, shortcut by shortcut.
Timeline and promise drive decisions that quality should be driving. You know it’s happening. You feel it. But the deadline is real, the stakeholders are waiting, and so you ship the band-aid and tell yourself you’ll come back and fix it.
You don’t come back and fix it.
The next sprint has its own shortcuts. And the one after that. Each one feels manageable in isolation. Collectively, they compound into something that embarrasses you. Slow apps, inconsistent behavior, features that technically work but don’t actually serve anyone.
We’ve been doing this for years. We just got really good at normalizing it.
Here’s the uncomfortable truth: the client in my 2014 project was initially happy with the delivery. The cracks didn’t show up right away. They never do. That’s what makes this pattern so easy to fall into. The consequences are delayed, but they’re never cancelled.
AI Didn’t Create This Problem. But It’s Making It Worse
Let’s be fair: AI is genuinely changing what’s possible for developers. Codex, Cursor, Claude. These tools are real, and the productivity gains are real. I’m not here to tell you to throw them out.
But here’s what I’m watching happen: we’ve taken a profession that already had a speed problem and handed it a turbocharger. We’re moving faster. We’re also making mistakes faster. And instead of using AI to close the gap between what we promise and what we deliver, a lot of us are using it to build more elaborate scaffolding around the wrong problems.
Agentic workflows. Autonomous pipelines. Multi-model orchestration. Some of it is genuinely useful. Some of it is us chasing the next shiny thing while our customers are still waiting for the thing we promised them two quarters ago.
The question worth asking is simple: what are we optimizing for? If the answer is “a faster dev cycle,” that’s not good enough. A faster dev cycle that produces the same shortcuts at higher velocity isn’t progress.
It’s just enshittification with better tooling.
The Complexity Tax
Frontend development is a decent example of where this gets out of hand. HTML and CSS have evolved into webpack configs, Vite setups, package.json files that look like they need their own documentation. To be clear, modern tooling exists for real reasons. Polished, functional UIs require it. But it’s worth occasionally asking whether the complexity we’ve introduced is serving the customer or serving our preference for a particular stack.
When the tools become the product, something has gone wrong.
Coming Back to Center
None of this means slowing down to the point of paralysis. It means being honest about what “done” actually means. It means asking, before you ship the band-aid, whether you’re solving the customer’s problem or just closing a ticket.
The rebuild I led in 2014 promised search that worked. We delivered search that barely limped along. The client felt it eventually. That gap between what you promise and what you deliver, customers always feel it eventually. The apps that feel sluggish, the features that half-work, the UX that technically functions but clearly wasn’t thought through. That’s the accumulated weight of a thousand decisions where speed won and quality lost.
We can use better tools. We can run tighter sprints. We can build smarter workflows. But none of that matters if we’ve accepted a definition of “shipped” that means “it works well enough that no one will complain today.”
The Challenge
Here’s what I’m putting in front of you: the next time you’re about to ship something you know isn’t right, stop for a second. Not to blow up the timeline, just to be honest about what you’re actually doing.
Are you solving a real problem for a real customer? Or are you closing a gap between a promise and a deadline and hoping no one notices?
We have to stop accepting shit as good enough. Not because some blog post told us to. Because we’re better than that, and our customers deserve better than that.
The bar we set for ourselves is the bar our software lives up to. What’s yours?
👉 If you enjoy reading this post, feel free to share it with friends!
Or feel free to click the ❤️ button on this post so more people can discover it on Substack 🙏
You can find me on X and Instagram.
Also, I just launched a new YouTube channel - Code & Composure


