Product Strategy ยท Engineering

Why the cheapest time to fix a wrong data model is right before launch

A rename or schema fix feels risky, but the risk comes from only two forces: live traffic and existing data. Before launch you have neither, so the fix is one migration and a refactor, and the window is about to close.

Vin Lim Founder, Astralab

A name is wrong. A customers table actually holds every contact who ever messaged the product, a status field models a workflow that has since changed, a model is called one thing and means another. Fixing it touches dozens of files, and someone asks the reasonable question: is it worth it, or do we ship now and clean up later? The answer is almost always fix it now, and the reason is timing, not tidiness. The cost of a correctness change like this stays roughly flat right up until your first production data exists, then climbs from there. The cheapest moment to make the change is the last moment before launch, and that window is usually about to close.

What actually makes a rename risky?

Almost everything filed under "renames are dangerous" traces to two forces, and only two. The first is live production traffic: code is reading and writing the old name right now, so a direct change breaks requests in flight. The second is an existing dataset: there are rows to migrate, backfill, and re-index, with lock and downtime cost. Plot a change on those two axes and the right technique falls out of the grid instead of out of fear.

No existing dataExisting data
No live trafficJust rename it. One migration plus a mechanical refactor. The cheap quadrant.Migrate the data, but it can be one shot. No dual-write needed.
Live trafficMostly direct. Coordinate the deploy so code and schema flip together.Expand-contract territory: add the new name beside the old, dual-write, migrate, cut over, drop the old.

The whole apparatus people associate with risky renames (expand-contract, parallel change, view aliases, three-phase migrations, dual-writes) lives in one cell: live traffic and existing data together. If you are not in that cell, importing its ceremony is cost you do not owe.

Why the pre-launch window is the cheap quadrant

A system with no production data and no live tenants sits in the top-left cell. Zero data removes both forces at once: nothing is in flight to break, and there are no rows to migrate. So a direct rename is one migration plus a find-and-replace refactor, and that is the entire job. You do not need a transition phase, and you do not need a view that preserves the old name. Those are answers to a problem you do not have yet.

One engine-level detail still bites even in the cheap quadrant. On PostgreSQL, renaming a table does not rename its indexes or its foreign-key constraints; they keep the old name embedded. A rename that stops at the table re-creates the same lying name one layer down. Finish the job: rename the indexes and constraints too (ALTER INDEX ... RENAME, ALTER TABLE ... RENAME CONSTRAINT), and verify the names on the real engine, because a green test suite on a different database (SQLite, say) will not exercise them. The behaviour is documented in PostgreSQL's ALTER TABLE reference.

Why the window closes at launch

The blast radius is roughly constant before launch and grows monotonically after it.

MomentWhat the same change costs
Before any production dataN files of mechanical change: one migration, one refactor, one test pass, one PR.
After launchN files, plus live data to migrate, plus external integrations now coupled to the old name, plus the expand-contract tax, plus the risk of touching a system real users depend on.

So "we'll do it later" is, in practice, a decision to pay several times over for the same change, easily 10x once migration and integrations are counted. Name the window out loud in the planning conversation, and decide before it closes. The strategic move is not heroics after launch. It is spending one cheap migration now, while the data is still empty.

Is it a real defect, or just bikeshedding?

This only fires for a genuine defect, and it is worth being strict, because renaming is easy to argue about. The test is the domain's own language. Code should use the same words as the people who understand the business, the idea Eric Evans calls a ubiquitous language. A name that means something different from what it says, a customers table that holds contacts, is a defect by definition: every reader has to translate it, forever.

The common escape hatch, "we'll just document that customers means contacts," makes it worse. It converts a one-time refactor into a permanent onboarding tax, and documentation that exists to apologise for the code is a smell. That is real technical debt, not bikeshedding. Parkinson's Law of Triviality (1957) describes the opposite failure, a team burning its energy on a trivial, arguable choice. "Is fetchData or loadData nicer" is bikeshedding; "this table says customer but holds anyone who ever messaged us" is a defect with a clear correct name. Only the second one is in scope.

YAGNI ("You Aren't Gonna Need It") is sometimes invoked to defend the bad name, and that is a misread. YAGNI bars building for imagined future requirements. A misleading name is not a speculative feature; it is a present defect, so YAGNI is silent. Keeping the wrong name is the right call in only three cases: the term is arguably fine and reasonable people disagree (then it is bikeshedding, leave it), the correct replacement is not actually settled yet (agree the name before you churn the code), or production data already exists, which moves you to the expensive cell where expand-contract, not a direct rename, is the right tool.

A four-question checklist before you rename

Run these in order. Any "no" that routes you away, stop there.

  1. Is the name semantically wrong against the language the domain experts use, so it actively misleads a fresh reader? If no, it is bikeshedding. Leave it.
  2. Is there live production data on it? If yes, this shortcut does not apply: use expand-contract or parallel change, not a direct rename.
  3. Is there a clear, agreed correct name with a free namespace? If no, settle the name first. Do not churn code around an unresolved term.
  4. All three favourable? Rename directly, now, atomically, before launch. One migration (table, columns, indexes, constraints), one mechanical refactor across the code, one test pass, one PR. Do not split a rename into phases; it is inherently all-or-nothing, and an atomic change avoids a broken intermediate contract.

None of this is about chasing pretty names. It is about a specific, recurring product decision: when a model is wrong and the fix is mechanical, the calendar, not the size of the diff, should drive the call. We make this exact call when we build, a base entity named for one thing that in practice held another, fixed in a single atomic change while the data was still empty. The change is cheap because the data is empty, correct because the name now tells the truth, and timed because the window was about to close. After launch it is none of those things.

Sources