Proving Behavioral Equivalence Beats Writing Code in Core Service Migrations
When migrating a core service, AI can generate most of the new implementation quickly. The hard part is proving the new system matches the old one’s implicit behavior. This article covers a six-step approach: freeze scope, extract specs and golden samples, constrain generation with tests, dual-run comparison, shadow traffic and staged cutover, and keep new features in sync—plus performance measurement, team skills, risks, and a launch checklist.
The bottleneck is proof, not generation
AI coding tools can already produce large amounts of code from a requirements description. For a core-service migration, the real bottleneck is not “writing it” but “writing it correctly.” Over years of evolution, the old system accumulates implicit behavior: how it handles bad input, timeout and retry boundaries, default values for missing fields, and timing under concurrency. Those behaviors are rarely documented, yet they affect users.
A successful migration therefore produces a reusable evidence chain that the old and new systems behave the same. The code is only one part of that chain.
Reframe the problem. Suppose a team rewrites an online request-handling service from a dynamic language into a memory-safe systems language. Nobody on the team knows the new language well, but AI can do most of the implementation. Human work should shift from writing every line to defining goals, constraints, verification, and go-live judgment.
Split work: people design and judge; AI implements
Keep roles explicit:
- People own system design: service boundaries, data model, interface contracts, failure modes, and rollout strategy. Distributed-system architecture is not something to outsource to a generator.
- People own the plan: break the migration into verifiable stages with clear exit criteria.
- AI owns concrete implementation: code, test scripts, comparison tools, and draft docs from the spec.
- People own the verdict: review diff reports and decide whether to increase traffic.
Generated code can look complete while details are wrong. You still need verification that does not rely on “it looks right.”
A six-step migration method
This framework applies whenever a new implementation replaces an old core service.
Step 1: Freeze scope—equivalence only
Scope creep is the usual failure mode. If you add features, change APIs, or tweak business rules during the rewrite, the old baseline blurs and diffs become unreadable.
- Freeze scope: the new system must match old behavior.
- New features land only on the old system until the migration is done, then they are ported.
- List what is out of scope, with reasons and how it will be handled.
This matters even more if the old system is still under active development. Step 6 covers keeping features in sync.
Step 2: Extract old behavior into a spec
The spec is the basis for verification. Without it, comparison is only human judgment. Spec sources typically include:
- API docs and code: request parameters, response shape, error codes, defaults.
- Production traffic samples: real input combinations, especially edges and invalid input.
- Historical bugs and tickets: past fixes often encode implicit rules that must be preserved.
AI can help read code, summarize branches, and propose tests; people must confirm the spec. A practical tactic is a golden sample set: sampled, sanitized production requests plus the old system’s full outputs as the baseline the new system must reproduce.
Step 3: Let AI implement under test constraints
Asking AI for the whole service often yields a reasonable structure with wrong details. Prefer tests first, then implementation.
- Turn golden samples into automated tests covering happy paths, boundaries, and error paths.
- Give tests and the API spec to the AI and implement module by module.
- Run tests after each generation; feed failures back until they pass.
- Review passing code by hand, focusing on concurrency, resource cleanup, error handling, and security.
Passing tests means known cases pass, not that all cases pass. That is why dual-run comparison exists.
Step 4: Build a dual-run comparison framework
Dual run sends the same request to old and new systems and compares outputs. It is the highest-value check because it follows real traffic, not imagined cases.
Core logic:
for each request r:old_resp = old_service.handle(r)new_resp = new_service.handle(r)old_norm = normalize(old_resp) # drop timestamps, request IDs, other non-deterministic fieldsnew_norm = normalize(new_resp)if old_norm != new_norm:record_diff(r, old_norm, new_norm)return old_resp # users still get the old result
Decide these up front:
- Normalization rules: which fields may differ (timestamps, random IDs, unstable list order). Too loose hides real bugs; too strict creates noise. Record every rule and get team agreement.
- Allowed-diff list: known acceptable differences (for example trailing float error) must be explicit, not silently ignored in the comparator.
- Diff categories: functional errors, performance differences, and harmless differences. Functional errors must go to zero before the next stage.
During dual run, the new system only computes and records; it does not affect user results.
Step 5: Shadow traffic and staged cutover
Shadow traffic copies real requests to the new system without returning its responses. Unlike dual run, it usually follows the full production path—network, caches, dependencies—and surfaces issues that appear only in production.
Cut over in stages with exit criteria:
| Stage | Traffic | Entry | Exit |
|---|---|---|---|
| Shadow | 0% (copy only) | Tests pass; comparison framework live | Functional diffs stay at zero for a defined window |
| Small cutover | ~1% | Shadow criteria met | Error rate and latency not materially worse |
| Ramp | Increasing by stage | Prior stage stable | No open diffs before 100% |
| Retire old | 100% | Full traffic stable for a period | Rollback still available; retire after approval |
Every stage needs a fast rollback switch. Keep the old service running during cutover; do not retire it early.
Monitor at least error rate, latency percentiles, resource use, and comparison-diff counts, using logging, metrics, and alerting.
Step 6: Sync new features during the migration
If the old system keeps shipping, the new one will lag. That is a common long-migration risk.
Combine:
- Feature register: every new feature must record impact on the new system and appear on the migration backlog.
- Static checks: analyze old-system changes for new branches, fields, and APIs and generate “new system must implement this” reminders.
- Contract tests: catch interface-level output changes immediately in the new system’s tests.
The goal is not to chase every product request in real time, but to give every new feature an owner and a plan so you do not discover a large gap on cutover day.
How to think about performance gains
A common mistake is committing to a specific speedup at project kickoff. Gains depend on load shape, bottlenecks, and implementation details; they are hard to predict beforehand.
- Baseline the old system: CPU, memory, first-byte latency, long-task duration.
- Run the same load on both systems with matching environment, data, and concurrency model.
- Separate short requests from long tasks; gains can differ a lot.
- Include capacity planning. If the old system scaled by adding resources on demand, the new one may need capacity planned in advance—that is part of migration cost.
Internal or public estimates are order-of-magnitude hints, not decision inputs. Decide from measurements in your environment.
Closing the skills gap
Lack of fluency in the target language should not delay the migration, but you need a plan:
- Review standards: even AI-generated code needs someone who can judge critical logic. Cover resource management, concurrency safety, error propagation, logging, and metrics.
- Minimal knowledge set: document the syntax, standard library, toolchain, and debugging needed for this migration.
- Experienced reviewers: bring in people who know the language for critical modules and reviews.
- Record decisions: capture problems and solutions so later owners can take over.
Skills usually grow quickly during the work. That does not replace formal review and verification, or you get a system that “runs” but nobody will touch.
Risks and boundaries
- Wrong but plausible code: AI may be perfect on the happy path and silently wrong on errors. Strengthen boundary tests and diff classification.
- Security and auth: authn, authz, and input validation are easy to oversimplify in a rewrite. Review those modules separately.
- Dependency change: libraries, runtime flags, and resource models differ; deploy and ops must change with them.
- Data consistency: if the service reads and writes shared data, isolate writes during dual run so the new system has no side effects.
- Rollback: keep the old system available during cutover and rehearse the rollback path.
Launch checklist
Before you start:
- Scope frozen; equivalence work separated from new features.
- Old behavior specified; golden sample set exists.
- Tests cover happy, boundary, and error paths; generated code passes them.
- Dual-run framework live; normalization and allowed diffs written down.
- Shadow traffic and staged cutover defined with entry and exit criteria.
- Rollback switch configured and rehearsed.
- Feature-sync process and owners in place.
- Performance claims based on your baselines, not external estimates.
- Security-sensitive modules reviewed separately.
Closing
With AI doing the rewrite, invest in the verification system. Generation is getting cheaper; proving equivalence is not. It still needs a clear spec, reliable comparison, careful cutover, and ongoing feature sync. Design those well and a team that does not yet know the target language can still migrate at a controlled pace.
