Strangler Fig Migration skill for Codex
Replace legacy one slice at a time, reversibly. Codex inventories every legacy entry point including cron jobs and direct database readers, ships a no-op routing facade first, then moves one slice at a time behind a flag. Parity is measured with shadow traffic or parallel runs, data ownership moves one entity at a time without dual writes, and nothing legacy is deleted until traffic evidence and your approval are in hand.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- strangler-fig-migration Installs to ~/.agents/skills/strangler-fig-migration/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install strangler-fig-migration The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project strangler-fig-migration Installs to .agents/skills/strangler-fig-migration/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/strangler-fig-migration.zipunzip -o strangler-fig-migration.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) strangler-fig-migration PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $strangler-fig-migration, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Plans and executes incremental replacement of a legacy system or module with the strangler fig pattern - a routing facade (reverse proxy, API gateway or in-code seam), branch by abstraction, slice-by-slice cutover behind flags, shadow traffic and parallel-run comparison, data migration via CDC or backfill, and final decommissioning. Use when asked to rewrite a monolith or legacy service, migrate endpoints to a new stack, replace a module without a big-bang cutover, or plan how to retire old code safely.
Use it when
- A legacy system or module must be replaced while it keeps serving users
- Someone proposes a big-bang rewrite and you want a safer path
- Endpoints or jobs are moving to a new stack one by one
- Old code paths need to be retired with evidence they are unused
Use something else when
- You are splitting the monolith into services and deciding boundaries: use microservice-boundaries
- The legacy code has no tests and you need to pin behaviour first: use legacy-code-characterization
How to check the result
For each slice, parity mismatches stay under the agreed threshold, error rate and p99 match the legacy baseline at every ramp step, and a staging rollback drill succeeds.
The mistake it prevents
Shadowing write traffic or dual-writing two stores from application code, which duplicates side effects and leaves data silently inconsistent.
Done well means the legacy system keeps serving users while the new one takes over one slice at a time, every slice can be routed back to legacy in minutes, behaviour parity is measured rather than assumed, and the old code is actually deleted at the end. A rewrite that runs both systems forever has doubled the maintenance cost.
Before planning
- Read
AGENTS.md, the legacy system’s entry points (routes, message consumers, cron jobs, batch scripts) and its data stores. Hidden entry points (nightly jobs, admin screens, direct database readers from other teams) are what sink migrations. - Build an inventory from evidence: access logs or gateway metrics give traffic per
route;
rgover routes and consumers gives the code list. Record for each entry point: traffic, business criticality, data it reads and writes, known quirks. - Find who else touches the legacy database directly (reporting, ETL, other services). Ask the user; query database grants or connection logs if available.
- Agree on the target and the reason (cost, velocity, end of support). Without a reason the migration stalls at 60%.
Choose the interception point
| Legacy shape | Facade | Notes |
|---|---|---|
| HTTP application | Reverse proxy or API gateway (nginx, Envoy, cloud LB rules) routing by path and method | Most common; route-level slices |
| Library or module inside one codebase | Branch by abstraction: interface in front of old code, new implementation behind a flag | Works without network changes |
| Message-driven system | New consumer on a separate subscription; route by message type or tenant | Watch ordering and duplicate processing |
| Batch jobs | Run new job in parallel writing to a shadow table, compare outputs | Cut over one job at a time |
| UI | Route by URL prefix to the new frontend; share session and navigation | Micro-frontend shell only if teams are separate |
Slice selection
Pick the first slice to prove the path, not to deliver the most value:
- Read-only, moderate traffic, well-understood behaviour, owned data.
- Then reads with complex logic, then writes, then the core write paths last.
- Slice by route, by tenant or customer cohort, or by percentage of traffic. Tenant slicing keeps a customer’s experience consistent; percentage slicing finds performance problems early.
Workflow
- [ ] Inventory entry points, traffic, data ownership, direct DB consumers
- [ ] Put the facade in front of legacy with zero behaviour change; deploy; verify
- [ ] Build slice 1 in the new system behind a flag (default: legacy)
- [ ] Shadow or parallel-run slice 1; compare responses; fix diffs
- [ ] Ramp slice 1: internal users, 1%, 10%, 50%, 100%, watching error and latency SLOs
- [ ] Remove the legacy code path for slice 1; repeat for the next slice
- [ ] Migrate data ownership; stop legacy writes; archive and decommission
Step 1: facade with no behaviour change
Route everything to legacy through the new facade first. Ship that alone and confirm latency and error rates are unchanged. Every later step is then a routing change.
upstream legacy { server legacy.internal:8080; }
upstream modern { server orders-v2.internal:8080; }
map $request_method$uri $orders_backend {
default legacy;
"~^GET/api/orders/[0-9]+$" modern; # slice 1: read single order
}
server {
location /api/orders/ { proxy_pass http://$orders_backend; }
location / { proxy_pass http://legacy; }
}
For percentage or tenant ramps, route in a gateway or in the application using the feature flag system, so rollback is a flag flip rather than a config deploy.
Step 2: branch by abstraction (in-process)
class InvoiceCalculator(Protocol):
def total(self, order: Order) -> Money: ...
def calculator_for(order: Order) -> InvoiceCalculator:
if flags.enabled("invoice-v2", tenant=order.tenant_id):
return NewInvoiceCalculator()
return LegacyInvoiceCalculator()
Step 3: prove parity
- Shadow (dark) traffic: send a copy of read requests to the new system, discard its response, log differences. Never shadow writes unless the new side writes to an isolated store; duplicated side effects (emails, charges) are real.
- Parallel run in-process: call both implementations, return legacy’s result, record mismatches with enough context to reproduce (GitHub’s Scientist library and its ports implement this pattern).
- Normalise before comparing: field order, timestamps, float formatting, legacy bugs you intend to fix. Keep an explicit list of accepted differences.
- Exit criterion: mismatch rate below an agreed threshold (for example under 0.1% over a week of traffic), every remaining mismatch explained.
Step 4: data
| Situation | Approach |
|---|---|
| New system can read the legacy schema for now | Share the database temporarily; new code owns no tables yet |
| New system needs its own store | Backfill, then keep in sync with change data capture from legacy until cutover |
| Writes must move | One writer at a time per entity; flip ownership per slice; sync back to legacy via CDC while legacy readers remain |
Avoid application-level dual writes (writing both stores in one request without a transaction); partial failures leave them inconsistent. If dual writes are unavoidable, add a reconciliation job that detects and repairs drift.
Step 5: decommission
Deleting legacy routes, tables or services is destructive. Before each removal, show the user evidence that traffic is zero (access logs, metrics over at least one full business cycle including month-end jobs), take a backup or snapshot, and get explicit approval. Remove in order: routing, code, scheduled jobs, then data after a retention period.
Gotchas
- Hidden callers: other teams’ scripts hitting legacy URLs or tables directly. Log unknown clients at the facade (user agent, source IP, API key) before cutover.
- Session and auth must work across both systems for the whole migration; plan shared sessions or token validation up front.
- Rewriting with “improvements” breaks parity checks. Port behaviour first, fix bugs in a separate, flagged change.
- Long-lived feature branches for the new system defeat the pattern; merge to main behind flags.
- The last 10% (admin tools, reports, edge cases) takes as long as the first 90%. Put those slices in the plan explicitly.
Verification
- Per slice: parity mismatch rate, error rate and p99 latency for the slice compared with legacy baseline, at each ramp step.
- Rollback drill: flip the slice back to legacy in staging and confirm it works, including data written by the new system during the ramp.
- At the end: zero requests to legacy for one full business cycle before removal.
Output template
## Strangler plan: <legacy> -> <target>
**Facade:** <proxy/gateway/abstraction>, deployed <date>, no behaviour change confirmed
| # | Slice | Traffic | Data | Parity method | Status | Rollback |
|---|---|---|---|---|---|---|
| 1 | GET /api/orders/{id} | 120 rps | read legacy DB | shadow, 0.04% diff | 100% modern | flag `orders-v2-read` |
**Hidden callers found:** <list>
**Data ownership plan:** <per entity>
**Decommission prerequisites:** <evidence needed, approvals> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Strangler Fig Migration"
short_description: "Replace legacy one slice at a time, reversibly"
default_prompt: "$strangler-fig-migration Plan how to move our PHP monolith's order endpoints to the new Go service without a big bang" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.