Should you rewrite your vibe-coded MVP or refactor it?

Refactor in most cases. If the app works, people use it and the data model broadly fits the business, improve it in place, one small step at a time, behind tests that record what it does today. Rewrite only a part whose foundation is wrong, such as a data model that cannot represent your customers or authorisation that was never designed in, and replace it piece by piece while the old version keeps serving users.

By Adapted from Chapter 7, System Architecture Patterns, of From Model to Product ·

What a working MVP knows that a rewrite does not

A rewrite is tempting when you open a codebase an AI tool produced over hundreds of prompts: very long files, the same logic in several places, names that changed meaning halfway through. Starting clean feels faster.

But an app that people depend on contains decisions. Every rule about prices, permissions, refunds, notifications and edge cases was settled somewhere, often by the tool itself when a prompt left the detail open. Few of those decisions are written down. Chapter 16 of From Model to Product, Working in Teams, warns about exactly this: hidden assumptions that nobody states and that stay invisible until real inputs arrive.

A rewrite, especially one generated afresh from a new prompt, decides all of them again, silently. Some come out differently, and your users find the differences for you. Chapter 7, System Architecture Patterns, argues for evolving a structure deliberately as pressure appears; this guide applies that to code an AI tool wrote.

The signals for each

Read your app against each row. Several refactor signals and one rewrite signal mean replacing one layer inside an app that otherwise stays, not starting again.

What you seePoints toWhy
The app works and users rely on it; the pain is how slowly it changesRefactorThe behaviour is right and the structure is the problem, and structure can change in small steps
Tables and fields broadly match the things your business deals withRefactorThe data model is among the hardest parts to change later, and yours is sound
Duplicated logic, huge files, no testsRefactorCommon in fast-built code, and fixable in place once tests pin the behaviour down
Everything runs as one deployable appRefactorA monolith is a sensible beginning, not a defect
Secrets in the client, or endpoints without authorisation checksFix now, in placeUrgent, but usually a set of local changes, unless permissions were never modelled at all
The data model cannot represent the business, such as one user per account when you sell to teams, or no way to keep one customer's data apart from another'sRewrite that layerEvery feature sits on it, so patching around it spreads the problem
Every change breaks something unrelated, and neither you nor the tool can say whyRewrite the worst part, behind testsCoupling so tight that small steps are impossible is the strongest structural signal
The platform cannot meet a requirement you now have, such as where data is stored or long-running background workMove or rewrite that partNo amount of tidying changes what the platform allows
Few or no users yet, and the rules are simple and written downRewrite freelyNothing depends on the old behaviour, so regenerating it is cheap

A messy monolith is not a reason to rewrite

One rewrite deserves a specific warning: splitting the app into microservices because that is what serious systems seem to look like. Chapter 7 argues that a monolith, one deployable application holding the API, business logic and data access, is often the right beginning. It has fewer moving parts, network boundaries, deployment units and service contracts, it is easier to debug, and it costs less to operate.

Microservices bring real benefits, such as independent scaling, clearer ownership and isolated failures, and real costs. Once a system is distributed, function calls become network calls, and retries, timeouts, circuit breaking, idempotency, observability and service discovery all start to matter. For an app that is already short of its operational layer, that multiplies the work. The chapter's verdict is that the mistake is not using microservices; it is using them before the boundaries are real enough to earn the added cost.

Chapter 7 finds the real boundaries with questions: which part changes often, which needs different scaling, which needs different hardware, which would be safer isolated, and which a separate team could own without constant friction. While the answers are blurry, keep one app and draw the boundaries inside it. AI system architecture shows how the layers fit together and when to split them.

Refactor state and boundaries first

Two structural problems are worth fixing before anything cosmetic, because they block both growth and safe change.

The first is hidden state. Chapter 7 asks whether any instance of a service can handle any request, or whether the system depends on state kept inside one particular instance. Fast-built code often keeps sessions, uploaded files, rate-limit counters, caches or job progress in server memory or on local disk. That works on one server and breaks when a second instance starts or the first restarts. Move that state into a database, a cache, a queue or object storage, so the app can run as several identical instances; scaling a vibe-coded app picks up from there.

The second is missing boundaries. Separate the code into a few modules with one job each: data access, business rules, and calls to external services such as payments, email and any model API. Put each external service behind a single function, so timeouts, retries or a replacement provider can be added in one place. Chapter 16 adds that architecture shapes how people work together: a modular system lets people work independently, a clear interface lets components change separately, and a well-defined data format reduces misunderstandings.

Pin down current behaviour before you change it

Refactoring means changing structure without changing behaviour, and you only know behaviour has not changed if something checks it. Before moving any code, write characterisation tests: tests that record what the app does today, including the parts that look odd, rather than what you think it should do.

Start with the flows that would hurt most if they broke: sign-up and login, payment, the core action people come for, and permissions, including a test that one user cannot read or change another's records. When a test exposes behaviour that looks wrong, do not fix it in the same step. Note it, ask whoever owns the business rule, and change it separately, so a refactor never hides a change in behaviour.

An AI tool can write these tests quickly, but read them. Asked to make a failing test pass, a tool can change the test instead of the code, so keep test changes and code changes in separate commits.

import { test } from "node:test";
import assert from "node:assert/strict";

// Copied from the app for this example. In your project, import it instead.
function seatDiscount(seats: number): number {
  if (seats >= 10) return 0.2;
  if (seats > 5) return 0.1;
  return 0;
}

// Record what the app does today, odd cases included.
test("five seats get no discount, six get ten percent", () => {
  assert.equal(seatDiscount(5), 0);
  assert.equal(seatDiscount(6), 0.1);
});

test("ten seats or more get twenty percent", () => {
  assert.equal(seatDiscount(10), 0.2);
  assert.equal(seatDiscount(500), 0.2);
});

test("zero and negative seat counts are not rejected", () => {
  assert.equal(seatDiscount(0), 0);
  assert.equal(seatDiscount(-3), 0);
});
A characterisation test with Node's built-in test runner. Save it as discount.test.ts and run node discount.test.ts on Node 22.18 or later, which runs TypeScript directly. The third test records a behaviour to raise with whoever owns the pricing rules.

If you do rewrite, replace one part at a time

When a layer has to go, replace it while the app keeps running, rather than building a second app in parallel and switching over on one risky day. Put a clear interface in front of the part you are replacing, build the new version behind it, move one route, feature or group of users across at a time, and delete the old code once nothing calls it. Martin Fowler calls this the strangler fig approach.

For logic with clear inputs and outputs, run the old and new versions on the same requests and compare the results before the new one serves anyone, the same idea as a shadow release for a model. A data model rewrite needs a migration rehearsed on a copy of production data, and a backup you have restored.

Keep the characterisation tests running throughout. They define done: the new part is finished when it passes the tests the old part passed, apart from the changes you decided on purpose.

Handing an AI-written codebase to an engineer

Bringing in an engineer often raises the rewrite question, because a codebase you did not write is easier to replace than to understand. Chapter 16 explains why the handover matters more than the code: many production problems come not from bad code but from assumptions that were never discussed. Give the engineer what the code cannot tell them.

  • The business rules in plain language: who can see and change what, how prices and limits work, what happens on cancellation, which emails go out and when. The code is not the specification, and neither are the prompts that produced it.
  • The assumptions you know you made, such as one currency, one time zone, English only or a few hundred users. Chapter 16's remedy for hidden assumptions is to state them and then check them with tests.
  • Shared definitions for the words your app uses, such as user, account, workspace, active or trial. The chapter's example is the word model, which means a neural network to one person, an endpoint to another and a feature to a third.
  • Access that belongs to the business: the Git repository and the hosting, database, authentication and model provider accounts, held by a company account rather than a personal login.
  • The characterisation tests, a list of known bugs and the parts you are least sure of.

Agree who owns what

Chapter 16 sets out the roles a product team needs: machine learning, data, backend, infrastructure, product, domain knowledge and testing. In a small team one person holds several, but the responsibilities still exist, so name who holds each. Often you keep product and domain knowledge, and the engineer takes backend, infrastructure and testing; if the app calls a model, someone also owns its behaviour and its bill.

Then apply the chapter's principle of shared ownership. It does not mean everyone does everything. It means the engineer understands the business rules well enough not to break them, and you understand the technical limits well enough not to promise what the system cannot do. A good first month for the engineer is reading, testing and fixing security and recovery; restructuring goes better once they know which odd behaviours are deliberate.

The healthiest architectural mindset is not to search for the most advanced pattern available. It is to search for the most appropriate structure for the product as it actually exists today, while leaving enough room for sensible evolution tomorrow.

From Chapter 7 of From Model to Product

Questions

When is starting again from scratch the right call?

When almost nothing depends on the current version: few or no real users, business rules that are simple and written down, and data you can migrate or discard. At that stage regenerating the app with what you have learned is cheap. Once people rely on it, replacing one layer at a time is safer than a full restart.

How do you refactor safely with an AI coding assistant?

Constrain it. Ask for one small change at a time, give it the characterisation tests and tell it they must keep passing, and read every diff. A large refactor requested in one prompt touches so much at once that a change in behaviour is hard to spot.

What is a characterisation test?

A test that records what existing code actually does, rather than what it was meant to do. You run the code on representative inputs, including odd ones, and assert exactly the output it gives today. The tests then warn you when a refactor changes behaviour. Michael Feathers coined the term in Working Effectively with Legacy Code.

What should an engineer review first in an AI-written codebase?

Security and recovery before style: where secrets live, whether every endpoint checks authorisation, how input is validated, whether production data is backed up and how a release is rolled back. Then state held in memory, external calls without timeouts and duplicated business logic. Naming and file layout come last, because they slow the team down without hurting users.

Is AI-generated code harder to maintain than code written by hand?

Not inherently. Any codebase is hard to maintain when logic is duplicated, tests are missing, boundaries are unclear and decisions were never written down, and fast prompting can produce all four quickly because each prompt solves only the problem in front of it. The remedies are the usual ones: tests, modules with one job each and written business rules.

How long should a vibe-coded app stay a monolith?

Until a part has a clear and pressing reason to leave: it has to scale on its own, needs a GPU the rest does not, changes far more often than everything else, or would be safer isolated. Chapter 7 notes that a small product benefits from being understandable first and distributed later, if later is even necessary.

About Dr. Dulan Dias