The replacement instinct
When you design a better data model for your system, the natural instinct is to replace the old one. Remove the legacy fields, migrate existing records, ship the new shape. Clean, complete, done.
This works fine when the migration is safe, fast, and reversible. It becomes dangerous when:
- The migration touches every record in a live production system
- The new model is not yet fully proven in all code paths
- The rollout cannot be completed in a single deploy
- You need old code and new code to run simultaneously during the transition
The additive pattern
Instead of replacing, extend. Add the new state alongside the old. Make the new state optional and derived — built from existing data when absent, but preferred when present.
// Before: flat legacy structure
const article = {
socialPosts: {
linkedin: "Here is the LinkedIn post",
twitter: "Here is the tweet",
}
};
// After: new structured state added alongside, not replacing
const article = {
socialPosts: { linkedin: "...", twitter: "..." }, // still there
distributionState: { // new, optional
linkedin: { textStatus: "ready", text: { body: "..." }, publishStatus: "ready" },
twitter: { textStatus: "ready", text: { body: "..." }, publishStatus: "ready" },
}
};
Code that reads the new state falls back to the old state when the new one is absent:
function getPostText(article, platform) {
// Prefer new structured state
const structured = article.distributionState?.[platform]?.text?.body;
if (structured) return structured;
// Fall back to legacy flat field
return article.socialPosts?.[platform] ?? null;
}
Three phases of additive rollout
Phase 1 — Coexistence. New state is written for new records and new operations. Old records keep working via fallback. Both states may exist simultaneously. New state is not required for anything to function.
Phase 2 — Preference. All reads prefer the new state when present. Generation and save flows start building the new state consistently. Legacy fallback is still active. New state is now the primary path for all records that have it.
Phase 3 — Authoritative. The new state is fully proven. Legacy fallback can be removed. Old fields can be deprecated. Records without new state can be lazily backfilled or explicitly migrated.
The key: do not move to Phase 3 until Phase 2 is stable in production, with real data.
Why "authoritative" is a milestone, not a starting point
The mistake is treating the new state as authoritative from the moment it is deployed. This forces every read path to have the new state present or fail — which means you need a complete migration before you can ship anything.
Treating the new state as additive first means:
- You can ship Phase 1 and 2 incrementally
- Real production data validates the new model before you depend on it
- If the new model has a gap, the legacy fallback catches it instead of breaking
- You can delay the deprecation decision until confidence is high
The invariant to preserve during rollout
The critical rule during additive rollout: do not delete or rewrite legacy state while the fallback is still active.
If legacy state disappears before the new state is proven everywhere, you lose both the primary path (not yet authoritative) and the fallback (deleted). This is how additive rollouts turn into silent data loss.
// ✓ Build new state alongside legacy, preserve legacy
await kv.set(articleKey(slug), {
...existing,
socialPosts: existing.socialPosts, // preserved
distributionState: newDistributionState, // added
});
// ✗ Do not remove legacy while still in Phase 2
// delete article.socialPosts; // too early — fallback still needed
When to skip the additive pattern
Not every migration needs three phases. The additive pattern is worth the overhead when:
- The new model is significantly different from the old one
- You have existing records in production that cannot all be migrated atomically
- Multiple code paths depend on the state (admin UI, API routes, generation flows, publish flows)
For simple field additions or renames with no fallback logic, a direct migration is usually cleaner.