Migrate large codebases to a new design system version — strangler fig pattern, incremental adoption, automated codemods, and risk management strategies for zero-downtime migrations.
Migrate large codebases to a new design system version — strangler fig pattern, incremental adoption, automated codemods, and risk management strategies for zero-downtime migrations.
The strangler fig pattern, named after the tree that gradually replaces its host, applies perfectly to design system migrations. Rather than rewriting all UI at once, new design system components replace legacy components incrementally — the new system 'strangles' the old one route by route, component by component. Implementation: both old and new design system coexist in the codebase during migration. Each product team migrates their area (checkout, account settings, product catalog) during normal sprint cycles. The legacy components are deprecated but not deleted until the last consumer migrates.
Key enabler: a compatibility bridge that makes new components accept the old API signature:
1// packages/bridge/src/ButtonBridge.tsx
2// Accepts both old API (kind prop) and new API (variant prop)
3import { Button } from '@acme/react-v2';
4import type { LegacyButtonProps } from '@acme/react-v1';
5
6const KIND_TO_VARIANT: Record<string, string> = {
7 primary: 'primary',
8 secondary: 'secondary',
9 danger: 'destructive',
10 ghost: 'ghost',
11};
12
13export function ButtonBridge({ kind, variant, ...props }: LegacyButtonProps) {
14 const resolvedVariant = variant ?? (kind ? KIND_TO_VARIANT[kind] : 'primary');
15 return <Button variant={resolvedVariant} {...props} />;
16}The bridge package lets teams do a step-by-step migration: first swap import paths, then migrate props at their own pace.
Sequence migration teams for minimum risk and maximum learning. Phase 1 (Pilot): migrate one low-risk, high-traffic page with an enthusiastic team. Learn from real issues. Document solutions. Phase 2 (Early adopters): the 3 teams closest to the design system (often the platform team and one or two product squads that contributed to the design system). These teams handle edge cases gracefully and generate the best feedback. Phase 3 (Mainstream): the majority of product teams migrate using the patterns from phases 1 and 2, automated codemods, and the compatibility bridge. Phase 4 (Cleanup): remove the compatibility bridge, delete the old design system package, run final codemods for any remaining manual patterns.
Timeline heuristic: at 100K lines of JSX/TSX, budget 3 months per phase. With good codemods, the mainstream phase compresses to 6 weeks. Never skip the pilot phase — it always discovers at least one edge case that would have caused a production incident in a broader rollout.
Well-written codemods handle 80% of migration changes automatically. Focus codemod effort on the highest-frequency patterns:
1// codemods/v2-migration/index.ts
2import type { Transform, JSXAttribute } from 'jscodeshift';
3
4const transform: Transform = (file, api) => {
5 const j = api.jscodeshift;
6 const root = j(file.source);
7 let changed = false;
8
9 // 1. Update import paths
10 root.find(j.ImportDeclaration, {
11 source: { value: '@acme/react-v1' }
12 }).forEach(path => {
13 path.node.source.value = '@acme/react';
14 changed = true;
15 });
16
17 // 2. Rename kind → variant on Button
18 root.find(j.JSXOpeningElement, { name: { name: 'Button' } })
19 .forEach(el => {
20 el.node.attributes.forEach(attr => {
21 if (
22 attr.type === 'JSXAttribute' &&
23 (attr as JSXAttribute).name.name === 'kind'
24 ) {
25 (attr as JSXAttribute).name.name = 'variant';
26 changed = true;
27 }
28 });
29 });
30
31 // 3. Replace className patterns for migrated components
32 root.find(j.JSXAttribute, {
33 name: { name: 'className' },
34 }).forEach(path => {
35 const value = path.node.value;
36 if (value?.type === 'StringLiteral') {
37 const updated = value.value
38 .replace(/\bbtn-primary\b/g, '') // Removed in v2
39 .replace(/\blegacy-card\b/g, 'card'); // Renamed
40 if (updated !== value.value) {
41 value.value = updated;
42 changed = true;
43 }
44 }
45 });
46
47 return changed ? root.toSource() : null;
48};
49
50export default transform;Test with 50+ fixture files representing real production patterns. The remaining 20% that codemods cannot handle (complex runtime conditions, dynamic component selection) become the documented manual migration items with step-by-step guides.
Track migration across all teams with an automated dashboard that scans every repository weekly:
1// scripts/migration-progress.ts
2interface TeamProgress {
3 team: string;
4 repo: string;
5 totalComponents: number;
6 v1Components: number;
7 v2Components: number;
8 adoptionRate: number; // v2 / total
9 estimatedWeeksToComplete: number;
10 lastUpdated: string;
11}
12
13async function calculateProgress(repos: string[]): Promise<TeamProgress[]> {
14 const progress: TeamProgress[] = [];
15
16 for (const repo of repos) {
17 const v1Imports = await countPattern(repo, /@acme\/react-v1/);
18 const v2Imports = await countPattern(repo, /@acme\/react[^-]/);
19 const total = v1Imports + v2Imports;
20 const rate = total > 0 ? v2Imports / total : 0;
21
22 // Based on historical velocity: ~50 component migrations per sprint
23 const weeksRemaining = Math.ceil(v1Imports / 50) * 2;
24
25 progress.push({
26 team: getTeamForRepo(repo),
27 repo,
28 totalComponents: total,
29 v1Components: v1Imports,
30 v2Components: v2Imports,
31 adoptionRate: rate,
32 estimatedWeeksToComplete: weeksRemaining,
33 lastUpdated: new Date().toISOString(),
34 });
35 }
36
37 return progress.sort((a, b) => a.adoptionRate - b.adoptionRate);
38}Post the dashboard to a shared Confluence page weekly. Sort by adoption rate ascending — teams at the bottom of the list see their position relative to peers, which gently motivates progress without requiring management escalation.
Design system migrations can surface unexpected issues: visual regressions, accessibility violations, or third-party integration failures discovered only in production. Build rollback capability:
CSS rollback: Maintain the old component CSS as a separate stylesheet. If a visual regression surfaces, revert the stylesheet import with a 1-line change:
1// Feature flag for CSS version
2// Can be toggled without a deploy via environment variable
3const cssVersion = process.env.DS_CSS_VERSION ?? 'v2';
4import(`/tokens/${cssVersion}/tokens.css`);Component rollback: Use the compatibility bridge as a rollback target. If a component causes issues in v2, roll back to the bridge version in under an hour:
1# Emergency rollback: swap one component back to bridge
2sed -i "s|import { Dialog } from '@acme/react'|import { Dialog } from '@acme/bridge'|g" src/**/*.tsx
3git commit -m 'emergency: rollback Dialog to bridge version'
4git push
5# Triggers automatic deploymentDocument the rollback procedure and make it executable in under 30 minutes by any on-call engineer without design system expertise. Test the rollback procedure quarterly in a staging environment.
Identify the three highest-frequency migration patterns in your design system upgrade. Write a jscodeshift codemod for each. Run them on a test branch and measure what percentage of the migration they complete automatically.
Your progress is saved in this browser, on this device only.