2
2 Comments

18 months in, our ship velocity dropped 3x. The problem wasn't the code.

I want to describe a specific failure mode, because I've now heard three other founders describe the same thing with completely different vocabulary, and I think we're all talking about the same disease.

Here's the shape of it. Somewhere between month twelve and month eighteen, shipping gets slow. Not dramatically — no single sprint where everything collapses. Just a steady, unremarkable decline that you don't notice until you compare a feature you shipped last week against a feature of identical scope you shipped last year. Ours took about three times longer. Same team, same stack, roughly the same complexity.

The instinct is to blame the codebase. Everyone on the team blamed the codebase. Our lead engineer had been saying "this thing needs a rewrite" for four months, and honestly, he had receipts — the test suite took nine minutes, we had three different patterns for handling API errors, and there was a service nobody wanted to touch because the person who wrote it had left.

We almost did the rewrite. What stopped us was an argument I lost.

The argument I lost

One of our engineers pushed back on the rewrite by asking a question that seemed almost insultingly basic at the time: which specific tickets took 3x longer, and what did they have in common?

Nobody had checked. We'd all been operating on vibes and frustration, which — I've since learned — is how most rewrite decisions actually get made.

So we spent two weeks going backward through about sixty closed tickets. Rough categorization, nothing scientific. What came out was uncomfortable.

The slow tickets weren't spread evenly across the codebase. They clustered. Roughly 70% of our worst time overruns touched one thing: how we'd modeled the relationship between a user and an organization.

Early on — like, week three of building, when we had zero customers and no reason to think about it — we'd decided a user belonged to exactly one organization. It was the obvious call. It was also wrong, and it became wrong the moment our first mid-size customer asked whether their consultant could access two workspaces.

We didn't rebuild the model. We shimmed it. Then we shimmed the shim. By month eighteen there were, by our count, eleven separate workarounds for that single assumption scattered across the codebase, and every new feature that touched permissions had to be built to accommodate all eleven. That's where the 3x lived.

The rest of the codebase was fine. Ugly in places, but fine. Ugly code is cheap. Wrong assumptions are expensive.

What we actually did

We didn't rewrite. We did a scoped rebuild of the identity and permissions layer — about 40% of the code by line count, though that number is misleading because it was maybe 15% of the surface area users could see.

Four months. Two engineers on it full-time, the rest of the team shipping normally. We ran the old and new models in parallel for six weeks with a translation layer between them, migrated customers in cohorts starting with our smallest accounts, and killed the old path once the last cohort was through. Two incidents, both minor, both caught before customers noticed.

A full rewrite, by our estimate, would have been somewhere north of a year with the whole team. I have no idea whether that estimate was any good. Rewrite estimates are famously fiction.

The part I'd tell my earlier self

The lesson isn't "don't rewrite." Sometimes you should. The lesson is that slow shipping is a symptom, and almost nobody diagnoses it before prescribing.

A rewrite is a very expensive treatment for a condition you haven't identified. If your velocity has dropped, the two weeks you spend auditing where the time actually went is the cheapest work you will do all year — and it's the only thing that tells you whether you're facing a diffuse problem (which a rewrite might fix) or a concentrated one (which a rewrite is wild overkill for).

There's a second, less comfortable lesson. That user-to-organization decision was made during MVP app development, by me, in about ninety seconds, on the reasonable grounds that we had no customers and needed to ship. I don't think that decision was wrong given what I knew. But I'd been told, like everyone gets told, that MVP decisions are cheap because you'll throw the MVP away. We didn't throw it away. Almost nobody throws it away. The MVP just quietly becomes the product, and the ninety-second decisions become the load-bearing walls.

So now I keep a short list — genuinely short, four or five items — of decisions I'd classify as structural. How identity works. How money moves. How data is scoped and isolated. Those get a day of thought even when we're moving fast, because they're the ones that don't stay cheap.

Everything else, I'll happily do badly and fix later. Ugly code is cheap.

Where I'm still unsure

I don't know whether we'd have been better off doing the full rewrite. We're eight months past the rebuild now and velocity recovered to roughly where it was at month twelve — not better, just recovered. There's an argument that we bought back the old speed and kept all the other debt, and we'll pay for it again in two years.

There's also an argument that the year we didn't spend rewriting is the reason we're still here.

Both arguments are unfalsifiable, which is the deeply annoying thing about this entire category of decision. You never get to run the control group.

If you've done the full rewrite — did the problems you were trying to escape actually stay gone? That's the thing I've never gotten a straight answer on.

posted toAvatar for product Healthcare App Development
Healthcare App Development
  1. 1

    The 70% figure is probably the most interesting part for me. Looking at where the time overruns were actually happening changed the problem from “our codebase is getting slow” to “this particular part of the product is creating most of the friction.”

    It makes me wonder how often teams look at velocity at the team/codebase level when the useful answer is actually sitting at the feature or domain level.

  2. 1

    The part that stands out is how the perceived problem was the codebase, while the actual pattern was concentrated around one early assumption. That distinction makes the rewrite question much more interesting.