A code migration is not complete when the new service compiles. It is complete when the team can show that the behavior users depend on still works—or that every intentional change has been reviewed. That makes migration a QA problem as much as a coding problem.
This tutorial shows how to use Codex for a small legacy-to-modern pilot while making parity evidence the gate between milestones. The example moves one invoice-rating flow from a legacy service to a modern API, but the method works for framework upgrades, language migrations, service extraction, and monolith decomposition.
Why parity must be defined before implementation
Two systems can return the same status code and still disagree in ways that matter: tax rounding, default values, error classification, record ordering, timestamps, retries, side effects, or latency under load. A weak assertion such as “response is not empty” can make a broken migration look green. Snapshot tests can do the same when a reviewer accepts a large update without understanding it.
Write a semantic parity contract before Codex changes code. List which outputs must be identical, which differences may be normalized, which changes are intentional, and which side effects must occur exactly once. For the invoice pilot, the contract might allow a new trace ID but require the same currency, line totals, tax rule, final amount, error class, ledger entry, and idempotency behavior.
Step 1: choose one observable pilot flow
Do not ask for a full-platform rewrite. Select a vertical slice with clear inputs, outputs, dependencies, and business value. The OpenAI modernization cookbook recommends starting with one pilot flow, then producing an overview, design, validation plan, and execution plan around it.
Create a disposable repository or branch with synthetic data. Record the Codex surface and build, repository, branch, base commit, legacy and target runtime or container digests, database schema, API contract version, fixture corpus hash, random seed, locale, time zone, character encoding, dependency lockfiles, allowed commands, network policy, time and cost budget, worktree identity, and reviewer. If any of these change, the evidence belongs to a different experiment.
Keep production credentials out of the lab. Put secret canaries in harmless fixtures so you can detect accidental disclosure without exposing a real key. Define the expected files, reports, and side effects. Commit, merge, deployment, data migration, and release remain human decisions.
Step 2: ask Codex to inventory the legacy behavior
Use an explore-only first phase. Ask Codex to trace the pilot from entry point to persistence, name its dependencies, locate tests and contracts, and distinguish observations from assumptions. Have it write four reviewable artifacts modeled on the official cookbook:
pilot_overview.mdfor boundaries, actors, and current behavior.pilot_design.mdfor the target architecture and compatibility approach.pilot_validation.mdfor parity rules, fixtures, metrics, and rollback.pilot_execplan.mdfor milestones, progress, decisions, and outcomes.
Seed legacy comments, logs, and issue text with an instruction-like prompt. The inventory must treat those strings as untrusted data, not permission. Compare the inventory with a human-authored dependency map. Missing a queue consumer, scheduled job, feature flag, or audit write is a migration risk even if the main endpoint passes.
Step 3: create a parity matrix and oracle
Translate the semantic contract into a matrix before generating the modern implementation. Use identical inputs for both systems and compare structured outcomes.
| Dimension | Fixtures | Oracle |
|---|---|---|
| Money | zero, boundary, fractional, multiple currencies | decimal value, scale, and rounding rule |
| Dates | DST change, leap day, midnight, missing zone | business instant and displayed zone |
| Errors | missing field, invalid state, dependency timeout | class, public code, retryability, no leaked detail |
| Ordering | ties, nulls, duplicate keys | documented stable order |
| Side effects | first call, retry, duplicate request | exactly-once ledger and notification rules |
| Performance | representative and stress datasets | agreed percentile budget, not one fast sample |
Normalization is dangerous when it hides a defect. Ignore a generated request ID only after proving it is non-semantic. Do not strip all timestamps, sort every array, coerce null to empty text, or round values merely to make comparisons pass. Version the normalizer and test it with counterexamples that must fail.
Step 4: anchor the target design to a contract
The official cookbook uses an OpenAPI contract as an anchor. Whether your project uses OpenAPI, protobuf, GraphQL, events, or database schemas, make the boundary machine-readable and version-controlled. Ask Codex to scaffold the smallest modern slice that satisfies the contract while preserving a compatibility adapter for the legacy caller.
Test the contract independently. Include required and optional fields, unknown fields, content types, size limits, authentication failures, version negotiation, and malformed input. Generate tests from the contract if useful, but retain hand-written business examples: schema validity does not prove correct tax, retry, or side-effect behavior.
Step 5: run a differential harness
Build a runner that sends the same immutable fixture to legacy and modern paths, captures raw responses and side effects, applies only approved normalization, and emits a structured diff. Each record should include fixture ID, input hash, legacy runtime identity, modern runtime identity, contract version, normalizer version, result hashes, duration, and verdict.
fixture -> legacy adapter -> captured result
\-> modern adapter -> captured result
-> semantic comparator -> evidence.json
Run happy paths, edge cases, validation failures, dependency timeouts, retry and idempotency cases, concurrency collisions, backfill samples, and rollback scenarios. Add a clean-control implementation and several known mutants—wrong rounding, duplicate side effect, changed null handling, reordered output—to prove the oracle catches real divergence. A harness that cannot kill known mutants is not strong enough to approve a migration.
Step 6: gate every migration checkpoint
Use short implementation loops and update the execution plan’s progress, decision log, and outcomes after each one. Require a reviewer to approve evidence before crossing a checkpoint.
| Checkpoint | Required evidence | Stop condition |
|---|---|---|
| Inventory | dependency map, current tests, unknowns | unmapped side effect or owner |
| Design | contract, parity matrix, compatibility and rollback plan | undefined intentional difference |
| Scaffold | contract tests, harness, clean control, mutants | oracle misses a seeded defect |
| Implementation | focused and full tests, differential report, reviewed diff | unexplained mismatch or skipped suite |
| Cutover rehearsal | shadow comparison, backfill and rollback timing | irreversible change or breached budget |
A green summary is not sufficient. Independently verify Git status, base and head commits, changed files, test command exit codes, report hashes, runtime identities, skipped-test counts, and generated snapshot updates. Rerun deterministic CI from a clean checkout rather than trusting only the agent session.
Step 7: test failure, interruption, and stale evidence
Interrupt Codex between implementation and validation. Restart with a changed base commit. Modify a fixture after the plan is approved. Simulate unavailable dependencies, a failed report upload, a partial backfill, an incompatible library, exhausted budget, and a rollback that restores code but not data. The workflow must mark old evidence stale and stop instead of carrying a previous green verdict forward.
Also test concurrent worktrees and workers. Record which checkout produced each artifact, ensure all workers share the intended base, and look for hidden changes outside the reported diff. Isolated worktrees reduce collision risk, but isolation does not prove correctness or automatically merge the right result.
Common false-parity traps
- Over-normalization: the comparator removes the field containing the defect.
- Fixture drift: legacy and modern systems receive similar but non-identical data.
- Weak assertions: tests check type or presence instead of business meaning.
- Approved snapshots: a bulk update converts regressions into the new expected result.
- Hidden side effects: responses match while duplicate messages or ledger writes occur.
- Environment mismatch: different locale, time zone, schema, or dependency versions explain the result.
- Partial migration: the new endpoint passes while a batch job still uses incompatible legacy behavior.
Practical migration scorecard
| Metric | Release expectation |
|---|---|
| Semantic parity | 100% for unchanged requirements; every approved difference linked to a decision |
| Oracle mutation detection | All seeded high-risk defects detected |
| Skipped or flaky tests | Zero unexplained skips; flakes quarantined, owned, and not counted as passes |
| Side-effect parity | No missing or duplicate externally visible effects |
| Evidence freshness | Matches current commit, runtime, contract, fixtures, and normalizer |
| Rollback rehearsal | Completes within the agreed recovery window with data reconciled |
| Performance | Agreed percentile and resource budgets met on comparable environments |
Release checklist
- One bounded pilot flow has an approved owner and boundary.
- Repository, runtimes, contracts, fixtures, policies, worktree, budget, and reviewer are frozen in evidence.
- The inventory separates observed facts from assumptions and covers every side effect.
- Semantic parity and allowed differences were defined before implementation.
- The differential harness uses identical inputs and a versioned, narrowly scoped normalizer.
- Clean controls and known mutants prove the oracle can fail.
- Happy, edge, error, concurrency, retry, performance, backfill, and rollback paths were exercised.
- Every checkpoint has current reports, hashes, exit codes, and an independently reviewed Git diff.
- Stale plans, interruption, injection, leakage, partial failure, and false-green evidence were tested.
- Architecture, commit, merge, deployment, data migration, and release remain human-controlled.
Final takeaway
Codex can accelerate the mechanical and investigative work of modernization, but speed is useful only when each milestone is reviewable. Start with one pilot, define semantic parity first, compare legacy and modern behavior on identical evidence, challenge the oracle, and stop at every unexplained difference. The safest migration is not the one with the most generated code; it is the one whose behavior, evidence, and rollback remain understandable to the humans responsible for release.
