Legacy code doesn’t retire gracefully. In logistics platforms, old PHP modules linger because they quietly support one carrier export, one admin report, or one bookmark ops still uses. They duplicate logic, confuse ownership, and make every refactor riskier.

We’d modernized visible surfaces (React workflows, new services), but deprecated paths remained live—sometimes hit by hidden scripts or old links. The result: split behavior, slow triage (“which path ran?”), and creeping coupling.

Goal: shrink the attack surface and clarify what’s production without a single customer or ops disruption.

Core Issues

  • Divergent logic between legacy and modern paths
  • Unknown consumers (bookmarks, scripts, old integrations)
  • Slower incidents from path ambiguity
  • Maintenance drag on shared deps and security patches
  • Hidden risk when touching anything “safe”

Debt that grows silently.

Real Constraints

  • Zero tolerance for downtime or workflow breaks
  • Need proof before permanent removal
  • Fast rollback if a surprise consumer surfaced
  • Incremental execution to build stakeholder trust
  • Ops/eng alignment on changes

Deprecation had to be treated like a release, not a git rm.

How I Approached It

Staged, evidence-first retirement:

Inventory & Risk Tiering
Cataloged candidates by traffic, deps, and impact. Low-risk: no hits recently. High-risk: touched shipment status or exports.

Runtime Evidence
Instrumented legacy entry points with structured access logs (module ID, caller context, timestamp). Observed real usage over weeks—killed assumptions fast.

Controlled Gates
For retirement candidates:

  • Wrapped in feature flags (off → fallback to legacy, on → modern path or 404)
  • Added explicit routing guards and graceful degradation
  • Kept old code live but dormant during stabilization windows

Dependency Hygiene
Extracted/duplicated only the truly shared utils into owned modules. Cut coupling so deletion wouldn’t cascade.

Per-Group Runbooks
Documented each wave: candidate list, monitoring queries, rollback toggle, stakeholder notes, removal criteria.

Waved Execution
Small batches → stabilize (log review + ops checks) → delete → clean references (menus, docs, scripts). Post-wave: watched for surprise noise.

Validation Mix

  • Tech: flag toggles in staging, fallback tests, log sweeps for unexpected hits
  • Operational: stakeholder walkthroughs on core flows (reports, exports, admin screens)
  • Post-rollout: incident patterns, support tickets, Slack noise—no spikes tied to waves

Stakeholders confirmed: “still works, just cleaner.”

Outcomes

  • Smaller, clearer codebase → faster onboarding and refactors
  • Less “which path?” debug time during incidents
  • Cleaner ownership and dependency graph
  • Lower long-tail maintenance (fewer legacy deps to patch)

Directional win: engineering spent less time second-guessing paths, ops had fewer “why this behavior?” questions.

Tradeoffs & Lessons

Safe is slower—instrumentation, waves, and alignment take calendar time vs. bulk delete. But regressions cost way more (emergency rollbacks, trust erosion).

Standouts:

  • Runtime logs beat git grep for “is it used?”
  • Rollback must be one toggle, not a revert commit
  • Cleaning references prevents zombie revivals
  • Waves build momentum—early successes unlock bigger ones

Next Level

  • Automated deprecation dashboards (traffic decay, dep count, last access) for objective scoring
  • Quarterly retirement cadence to keep debt low
  • Extend to strangler-style redirects for remaining high-risk legacy surfaces

Deprecation isn’t glamorous, but done this way it quietly accelerates everything else. Less uncertainty means faster, safer shipping.