1. Wrapping, Not Extracting
Putting a new API layer over the legacy system without moving business logic out. The legacy database remains the source of truth. The new layer becomes a translation facade that must change whenever the legacy changes.
Modernization projects fail in predictable ways. The same mistakes repeat across organizations, technology stacks, and team sizes. Recognizing these anti-patterns before you fall into them is more valuable than any technique for getting out of them afterward.
Each anti-pattern below describes the trap, explains why teams fall into it, and provides the alternative approach.
1. Wrapping, Not Extracting
Putting a new API layer over the legacy system without moving business logic out. The legacy database remains the source of truth. The new layer becomes a translation facade that must change whenever the legacy changes.
2. UI-First, API-Later
Building the new frontend before the new backend exists. The frontend couples to legacy APIs, which freezes the legacy API surface and prevents backend modernization.
3. Big Bang Rewrite
Attempting to rewrite the entire system before shipping anything. No production feedback for months or years. Scope creeps. Teams lose morale. The project gets cancelled.
4. Shared Development Database
Old and new systems reading from and writing to the same database during migration. Creates tight coupling, prevents schema evolution, and makes independent deployment impossible.
5. Verbal-Only Architecture
Making migration decisions in meetings without writing them down. Decisions get lost, repeated, or contradicted. New team members have no context.
6. Accuracy Without Baselines
Claiming “95% parity” without defining what 100% means. Self-assessed progress creates false confidence. The remaining “5%” turns out to be 40% of the effort.
7. Migrating Everything
Treating every entity, feature, and module as equally important. Spending the same effort on a rarely-used report as on the core transaction engine.
8. Ignoring the Framework
Treating migration as a language translation problem. The real challenge is extracting business logic from framework coupling, not converting Python syntax to Go syntax.
A team builds a new API that calls the legacy system underneath. The new API has clean endpoints and modern documentation, but every request ultimately hits the legacy database through legacy code.
┌────────────────┐ ┌────────────────┐ ┌────────────────┐│ New API │────▶│ Thin Adapter │────▶│ Legacy System ││ (looks modern) │ │ (pass-through) │ │ (still runs) │└────────────────┘ └────────────────┘ └────────────────┘Extract business logic into the new system with its own data store. The new system owns the behavior and the data. Use the strangler fig pattern with an anti-corruption layer that is designed to be temporary.
The team builds a new React/Vue/Angular frontend against the legacy API. The frontend looks modern, but it is tightly coupled to legacy API response shapes, error codes, and implicit conventions.
Design the new API first. Build it against the new domain model. The new frontend targets the new API from day one. Use parity testing to verify the new API matches legacy behavior. The old frontend continues working against the old API until the new one is ready.
“Let’s just rewrite it from scratch. We understand the domain now. It’ll be faster than migrating incrementally.”
The team spends 12-18 months building the new system in isolation. No production traffic. No user feedback. The legacy system continues diverging as business requirements change.
Use the strangler fig pattern. Migrate one bounded context at a time. Ship each extraction to production. Get feedback. Adjust. The legacy system shrinks incrementally until nothing remains.
The old system and new system share the same database during migration. The new system reads legacy tables. The old system reads new tables. Both write to both.
┌────────────────┐ ┌────────────────┐│ Legacy System │──────┬───▶│ Shared DB │└────────────────┘ │ │ │ │ │ old_tables │┌────────────────┐ │ │ new_tables ││ New System │──────┘ │ hybrid_views │└────────────────┘ └────────────────┘Each system owns its own data store. Use a synchronization mechanism (CDC, events, sync jobs) to keep them consistent during the transition. Accept eventual consistency for the migration period. This is the dual-write infrastructure described in Strangler Fig.
Migration decisions happen in meetings, Slack threads, and hallway conversations. There is no written record of:
Document decisions in the spec. ModernizeSpec’s extraction-plan.json records sequencing decisions. migration-state.json tracks current status. domains.json captures boundary decisions. These are machine-readable, version-controlled, and survive team changes.
Architecture Decision Records (ADRs) capture the why behind decisions: what options were considered, what was chosen, and what the trade-offs were.
“We’ve achieved 95% parity on the invoicing module.” But there is no definition of what 100% means. The 95% is self-assessed based on the tests the team chose to write, not measured against a comprehensive baseline.
Capture baselines first using characterization tests. Record legacy system outputs for comprehensive inputs before building the new implementation. Define 100% as “all baseline outputs match.” Measure parity against this baseline, not against team intuition.
Use confidence scoring to quantify how much of the baseline is covered.
“We have 521 entity types. We need to migrate all 521.” Every module, report, and feature gets the same priority. The team spends equal effort on a rarely-used asset depreciation calculator and the core invoicing engine.
Prioritize by business value and usage. Use runtime evidence to identify which modules are actually used in production. Use hot path identification to find the 20% that matters.
| Priority | Criteria | Action |
|---|---|---|
| P0 | High traffic, core business | Migrate in early phases |
| P1 | Medium traffic, standard business | Migrate in mid phases |
| P2 | Low traffic, used monthly/quarterly | Migrate in late phases |
| P3 | Zero traffic in 90+ days | Do not migrate. Challenge the requirement |
Record priority assignments in extraction-plan.json phases.
“We’re converting Python to Go. How hard can it be?” The team treats migration as a syntax translation problem. They convert Python functions to Go functions line by line.
Legacy code does not live in isolation. It lives inside a framework that provides:
| Framework Service | Impact on Migration |
|---|---|
| ORM and database abstraction | Every database call uses framework patterns |
| Permission system | Authorization checks are implicit, not explicit |
| Hook/event system | Business logic fires implicitly through lifecycle events |
| Multi-tenancy | Tenant isolation is handled by the framework, not the code |
| Caching | Framework manages cache invalidation transparently |
| File storage | File handling uses framework abstraction layers |
| Background jobs | Task scheduling is framework-managed |
Translating a function from Python to Go produces a function that does not have any of these framework services. It compiles but does not work.
Map framework dependencies explicitly using codebase analysis. For each function or module:
The extraction is the hard part. The translation is the easy part.
All eight anti-patterns share a root cause: premature action without sufficient understanding. The team starts building, converting, or wrapping before deeply understanding the legacy system’s behavior, boundaries, dependencies, and runtime characteristics.
ModernizeSpec exists to ensure that understanding comes first. The specification files — domains.json, complexity.json, extraction-plan.json, parity-tests.json — are the artifacts of understanding. Populating them forces the team to answer hard questions before writing migration code.
The techniques in this section (Codebase Analysis, Domain Decomposition, Runtime Evidence, Parity Testing) are the tools for building that understanding. The anti-patterns are what happens when you skip them.