The Prototype That Shipped
There’s a special class of code that exists in every production system. It was written quickly, with the explicit understanding that it was temporary. A proof of concept. A “let’s see if this works” sketch. A prototype.
And then it shipped. And it’s still running.
How It Happens
The timeline is always roughly the same.
Someone needs to validate an idea. Maybe it’s a new feature, maybe it’s an integration with an external service, maybe it’s a data pipeline that needs to handle a format nobody’s encountered before. The right thing to do is design a proper solution, spec the interfaces, write the tests, handle the edge cases.
But there’s a deadline. Or there’s uncertainty about whether the approach even works. So someone writes a quick version. Hardcoded values, no error handling, maybe a comment that says // TODO: make this real or # temporary hack, revisit before release. It works. The demo goes well. Everyone’s impressed.
Then the next sprint starts, and there are new priorities. The prototype is working. Nobody’s complained. The TODO comment quietly ages. Six months later, someone new encounters the code and asks about it. “Oh, that was supposed to be temporary,” someone says. “But it works.”
And it does. It works.
The Uncomfortable Truth
Here’s what nobody tells you about prototypes: sometimes they’re the right solution.
Not because they’re well-written. Not because they handle edge cases gracefully. Not because they’re maintainable or testable or elegant. But because they solve the actual problem at the actual scale with the actual constraints that exist right now.
Software engineering is full of imagined futures. We architect for scale we don’t have. We abstract for flexibility we don’t need. We build frameworks to handle variations that never materialize. And in the process, we sometimes spend weeks building a “proper” version that does exactly what the prototype did, just with more indirection and more code to maintain.
The prototype that shipped isn’t always technical debt. Sometimes it’s accidental pragmatism.
When to Let It Stay
There are signals that a prototype has earned its place:
It hasn’t broken. If the code has been running in production for months without incident, that’s empirical evidence of correctness. Not proof — absence of failure isn’t proof of correctness — but evidence. It means the code’s assumptions happen to match reality, at least for the inputs it’s seen.
The scope hasn’t changed. If the prototype handles exactly one case and that’s still the only case that exists, rewriting it to handle theoretical future cases is speculative engineering. You’re spending real time today to save imagined time tomorrow.
The blast radius is small. If the prototype is isolated — a single function, a standalone script, a self-contained module — the cost of it being messy is contained. It’s not infecting the rest of the codebase with its approach.
Nobody’s actively working near it. Code that nobody touches doesn’t slow anyone down. A prototype buried in a corner of the system that processes one specific edge case isn’t blocking anyone’s velocity. Rewriting it for cleanliness alone is activity without impact.
When to Replace It
But prototypes can also be ticking time bombs. The signals for replacement are equally clear:
The assumptions are drifting. The prototype was written for 100 records per day and now there are 10,000. It was written for one data format and now there are three. The gap between what the code assumes and what reality demands is growing.
People keep having to work around it. If every new feature in the area requires understanding why the prototype does something weird and routing around it, the cost is no longer contained. The prototype is taxing every change in its vicinity.
The error cases are becoming real. The prototype that doesn’t handle errors is fine when errors don’t happen. When they start happening — and in production, they eventually do — the lack of error handling transforms from a theoretical concern into an operational one. Silent failures, corrupted data, mysterious downstream effects.
It’s become load-bearing. The most dangerous prototypes are the ones that other code starts depending on. When the prototype’s output format becomes an implicit contract, or its timing becomes an assumption in another system, replacing it gets harder with every passing week.
The Rewrite Trap
Here’s the thing about replacing prototypes: the rewrite is almost always harder than expected.
The prototype works. It handles the real-world cases that actually occur. It might not handle them elegantly, but it handles them. The cases it handles are documented implicitly in the code itself — every conditional, every special case, every weird workaround reflects a real situation that somebody encountered.
A rewrite starts from a specification, and specifications are incomplete by nature. They capture the known cases but miss the discovered ones. The developer writing the replacement has to understand not just what the prototype does, but why it does it, including all the subtle behaviors that nobody documented because they seemed obvious at the time.
This is why rewrites frequently introduce regressions. Not because the developer is careless, but because the prototype contains institutional knowledge that’s easy to overlook and hard to extract.
The Middle Path
The best approach to prototype code isn’t “rewrite it properly” or “leave it forever.” It’s incremental improvement based on actual need.
When you need to modify the prototype for a real reason — a new requirement, a bug fix, a performance issue — improve it then. Add the error handling that’s now necessary. Refactor the piece that’s in your way. Write a test for the behavior you’re about to change.
This way, the code improves in proportion to how much it matters. Heavily-used code gets heavily improved. Code that nobody touches stays as-is, because the cost of its messiness is zero.
It’s not glamorous. There’s no satisfying “big rewrite” moment. But it’s efficient in the way that matters: it directs engineering effort toward actual problems rather than aesthetic preferences.
The Lesson
The prototype that shipped teaches a useful lesson about software engineering, which is that the relationship between code quality and code value is not linear.
Beautifully written code that solves the wrong problem has negative value. Ugly code that solves the right problem and keeps running has significant value. Quality matters — but it matters in proportion to how much the code is read, modified, and depended upon.
Sometimes the right decision is to let the prototype keep running and spend your engineering time somewhere it’ll have more impact. That’s not laziness. That’s resource allocation.
Write // still the prototype, still works and move on. There are bigger problems to solve.