Rewrites are usually blamed on technical debt. In practice, the trigger is more specific: the business needed a feature that the data model had no place to put. At that point the choice is between a migration under load and a rewrite, and both are expensive.
1. Tenancy added after the first customer
A single-tenant product is simpler to build and cheaper to launch, which is exactly why it happens. The problem is that tenancy is not a column you add later — it is a dimension that touches every query, every unique constraint and every permission check.
-- Retrofitting tenancy means revisiting every unique constraint
-- NOT unique: email
-- Uniqueness is: (tenant_id, email)
-- And every index, every query, and every cache keyIf multi-tenant is anywhere on the roadmap, the tenant identifier belongs in the schema from the first migration, even while there is exactly one tenant in the table.
2. Entitlements modelled as boolean flags
The first version usually has a plan column and a handful of feature flags. That works until you need usage-based pricing, trials, add-ons, or a customer who has negotiated something bespoke. Now pricing logic is scattered across the codebase in conditionals.
Model entitlements and metered usage as data from the start, and keep the billing provider synchronised to your model rather than being the source of it. That way changing provider is an integration change, not a data migration.
3. History stored as current state
A user table with a role column holds the current role. It does not hold who granted it, when, or what it was before — which is precisely what an auditor asks, and what you need when investigating an incident.
- Append-only records for anything financial or permission-related.
- Actor, timestamp and reason on every privileged change.
- Never delete; transition to a new state and record why.
This is cheap when built alongside the feature and near-impossible to reconstruct afterwards, because the missing history is gone.
4. Flexible data modelled as opaque JSON
A JSON blob column feels like a way to avoid committing to a schema. It becomes a liability the moment anything needs to query, validate, aggregate or report on those fields — because now the database cannot help you and the application must defend against every shape ever written.
Use JSON for genuinely open-ended data. Model it properly the moment the business starts reporting on it — and something always gets reported on.
How to actually make these decisions
Write the domain model down before writing application code, and then argue with it. Specifically, for each entity, ask three questions: who else might own one of these, what history must survive, and what will finance want to report on. Those three questions surface most of the expensive mistakes while they are still cheap.
The output is short — a few pages — and it is the highest-return document a product team produces. It costs a week and occasionally saves a year.
