Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 60

    Patterns in Legacy Codebases

    Patterns in legacy codebases is where seniors earn their keep: no tests, tangled dependencies, original authors gone, business can't stop shipping.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Patterns in legacy codebases is where seniors earn their keep: no tests, tangled dependencies, original authors gone, business can't stop shipping. Michael Feathers' playbook: characterize first, introduce seams, apply patterns incrementally — Adapter, Facade, Strangler — never big-bang.

    The story

    Team inherited 30,000-line PHP monolith, zero tests, 12 years of patches. Three months of characterization tests, then seams via Adapter and Facade. Over 18 months 60% replaced with TypeScript microservices via Strangler Fig — zero production incidents.

    The business problem

    Legacy traps teams between rewrite fantasy and change paralysis:

    • No tests: every edit is roulette.
    • Tangled deps: touch one class, break ten.
    • Lost knowledge: authors gone; behavior undocumented.
    • Business pressure: can't freeze for "great refactor."

    The problem teams faced

    Legacy pattern application requires:

    • Characterization tests before any structural change.
    • Seams — places to change behavior without editing call sites.
    • Incremental patterns: Adapter, Facade, Strangler — not rewrite.
    • ACL at legacy boundary — don't leak foreign vocabulary inward.

    Understanding the topic

    Intent: Change legacy safely via seams and incremental patterns.

    • Characterization — record outputs; tests describe what is, not what should be.
    • Seam — inject dependency, extract interface, wrap with Adapter.
    • Adapter/Facade — simplify legacy surface for new code.
    • Strangler — route new features to new stack at edge.

    Internal architecture

    Adapter seam at legacy ERP:

    ts
    // New domain code — clean vocabulary
    interface InventoryPort {
    reserve(sku: string, qty: number): Promise<ReservationId>
    }
    // Adapter wraps legacy ERP XML/RPC mess
    class ErpInventoryAdapter implements InventoryPort {
    constructor(private legacy: LegacyErpClient) {}
    async reserve(sku: string, qty: number) {
    const raw = await this.legacy.call("INV_HOLD", { sku, qty }) // foreign API
    return ReservationId.parse(raw.hold_id) // translate inward
    }
    }

    Visual explanation

    Three diagrams cover Feathers workflow, seams, and strangler combo:

    Feathers legacy workflow
    Characterize
    Pin behavior
    Find seam
    Injection point
    Adapter/Facade
    Pattern
    Strangler route
    New stack
    Never edit legacy without characterization safety net.
    Seam types
    Extract interface
    Dependency injection
    Wrap legacy
    Adapter
    Simplify API
    Facade
    Change without edit
    Call site stable
    Seam = change behavior without changing every caller.
    18-month PHP migration
    0 tests → characterize
    3 months
    Adapters at ERP
    ACL
    Strangler routes
    NGINX
    60% TypeScript
    0 incidents
    Story combines characterization, Adapter, Strangler — not rewrite.

    Informative example

    Before / after — direct legacy call to Adapter seam:

    ts
    // ❌ New feature calls legacy ERP directly — vocabulary leaks
    const hold = await legacyErp.call("INV_HOLD", { sku, qty })
    // ✅ Adapter seam — new code speaks domain
    class OrderService {
    constructor(private inventory: InventoryPort) {}
    async place(order: Order) {
    await this.inventory.reserve(order.sku, order.qty)
    }
    }
    // Legacy isolated in ErpInventoryAdapter — characterize adapter thoroughly

    Execution workflow

    1Legacy pattern workflow
    1 / 4

    Characterize

    Record current outputs as expected.

    3 months on 30k LOC story.

    Real-world use

    Working Effectively with Legacy Code (Feathers); Strangler Fig (Fowler); ACL (Evans DDD); characterization tests in Rails/Java legacy migrations.

    Production case study

    30k-line PHP monolith migration:

    • Start: zero tests; 12 years patches.
    • 3 months: characterization tests across critical paths.
    • 18 months: 60% TypeScript via Adapter + Strangler; 0 incidents.

    Trade-offs

    • Pro: continuous delivery; no big-bang; business keeps shipping.
    • Con: characterization slow upfront — pays back.
    • Con: dual-run complexity during strangler period.

    Decision framework

    • Never refactor legacy without characterization on touched paths.
    • Adapter at every legacy integration — ACL inward.
    • Strangler for route-level migration; Adapter for vocabulary.

    Best practices

    • Pin legacy behavior before Adapter rewrite.
    • New features on new stack when strangler route exists.
    • Document legacy quirks in adapter tests, not domain.

    Anti-patterns to avoid

    • Big-bang rewrite "we'll catch up on tests later."
    • Edit legacy globals without seam — shotgun surgery.
    • Skip ACL — ERP field names in domain entities.

    Common mistakes

    • Characterization tests that encode bugs as correct.
    • Adapter grows business logic — becomes new monolith.

    Debugging tips

    • Characterization diff after change — intentional or regression?

    Optimization strategies

    • Characterize highest-churn legacy modules first — ROI.

    Common misconceptions

    • Legacy patterns ≠ rewrite — incremental only.
    • Characterization ≠ approval — documents reality.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionCharacterization test vs unit test?+

    Answer

    Characterization: record what legacy actually outputs now — may include bugs. Unit: specify correct behavior. Use characterization before legacy refactor.

    Follow-up

    Encoding bugs?
    2AdvancedQuestionWhat is a seam in legacy code?+

    Answer

    Place you can change behavior without editing all call sites — DI, override, wrap with Adapter. Feathers' key concept.

    Follow-up

    Find seam without tests?
    3IntermediateQuestionAdapter vs Strangler in legacy?+

    Answer

    Adapter: wrap legacy API for new code at boundary. Strangler: route traffic to new system at edge. Often used together — Adapter at integration, Strangler at HTTP.

    Follow-up

    PHP story?

    Summary

    You can apply Feathers' workflow, place Adapters at legacy seams, and pair with Strangler. Tell the 30k PHP zero-incident story — if you can do that, you own Patterns in Legacy Codebases and complete Refactoring & Anti-Patterns.

    Ready to mark this lesson complete?Track your journey across the entire course.