Better Coaching

Replacing a Legacy PHP Backend Module by Module Without Freezing Product Development

Stack

  • Node.js
  • Express
  • PHP
On this page

Key results

Legacy PHP API replaced
100%
Migration time
~5–6 months
Feature freezes
0
Big-bang cutover
No

Problem

Better Coaching is a multi-tenant SaaS platform that football clubs and academies use to manage coaching, players and training content.

The problem wasn’t PHP itself. It was the inherited backend and the way it had evolved. Business logic often lived directly in controllers, patterns differed between parts of the application, validation and error handling were inconsistent, responsibilities were tightly coupled, and some data and logic were duplicated.

Even small product changes were hard to reason about. Developers had to understand several interconnected pieces of legacy behavior before touching a feature, which raised the risk of regressions. The platform was seeing frequent bugs and support requests, and continuing to patch the existing code was no longer sustainable.

Better Coaching needed a backend that was easier to maintain, extend and scale, with one hard constraint: product development couldn’t be put on hold for months while it was rebuilt. Customers were already using the platform and new requirements kept arriving, so the modernization had to happen alongside normal product work.

My role

I was the team lead on this migration. I set the strategy of replacing the backend incrementally instead of rebuilding it behind closed doors, and led the team through moving the API from PHP to Node.js and Express one module at a time.

I also balanced the migration against the product roadmap, so the team could keep shipping features while working across two generations of the backend.

Technical approach

Migrating module by module

We divided the backend into features and modules and migrated them progressively. For each module, we:

  • analyzed the existing PHP implementation;
  • identified the business behavior it actually needed;
  • rebuilt it in Node.js and Express;
  • refactored problematic logic instead of copying it;
  • improved validation and error handling;
  • tested the migrated functionality;
  • switched the application to the new implementation once it was safe.

During the transition both APIs ran in parallel. Some parts of the platform were still served by PHP while migrated features already talked to the Node.js backend, and responsibility moved away from the legacy system one module at a time.

Rebuilding the architecture, not the syntax

Translating PHP into JavaScript line by line would have changed the language without fixing the maintainability problems. Instead, controllers were kept to HTTP concerns:

  • receiving requests;
  • validating input;
  • calling the right application logic;
  • returning responses.

Business logic moved into dedicated services and modules, and validation, error handling and patterns became consistent across modules. Node.js itself wasn’t the fix. A cleaner structure that the team could evolve safely was.

Shipping features during the migration

Product work continued throughout. New requirements arrived, existing features needed maintenance and new features still had to ship, so the team was effectively working with two generations of the backend at once.

A feature could depend on the legacy API while a neighboring module had already moved to Node.js, and new development had to account for where each part of the system lived now and where it would live after the migration. That added coordination, but cost far less than freezing the roadmap.

Validating each module before switching

Each migrated area was validated before the application depended on it, using:

  • development and staging environments;
  • manual QA;
  • comparison against the legacy implementation’s expected behavior;
  • automated tests where applicable.

Only once a module was stable did the application move fully to its Node.js version, so problems stayed isolated to small areas instead of surfacing in one large cutover.

Decisions & trade-offs

Avoiding a big-bang rewrite

The alternative was to rebuild the whole backend separately and switch from PHP to Node.js in one go. That would have concentrated all the risk into a single moment. The new backend would have had to reproduce years of existing behavior before real users could validate it, and any missed edge case would only appear after everything had switched.

Running both APIs in parallel and moving module by module traded some coordination overhead for small, recoverable steps.

Preserve or fix legacy behavior?

Rebuilding a module wasn’t just reading the old code and reproducing it. Some legacy behavior was unclear, some existed because of historical implementation choices rather than product needs, and some was already causing bugs. Every module came back to the same question:

Should we reproduce the old behavior exactly, or fix it while rebuilding the feature?

Copying everything would preserve existing problems, and changing too much at once would raise migration risk. Where behavior was a genuine product requirement, we kept it. Where the implementation was the problem, we refactored it while keeping the user-facing behavior intact. That made the migration slower than a syntax conversion, but it’s why the new backend is actually easier to maintain.

Results

Legacy API fully retired

After roughly five to six months, every backend module had been migrated and the PHP API was switched off. The platform wasn’t left half-modernized with lingering dependencies on the old system.

Product work never stopped

Better Coaching didn’t have to choose between modernization and its roadmap. New features kept shipping while modules moved, and the migration delivered improvements continuously instead of after months behind closed doors.

Risk spread across small releases

There was never a moment when the whole backend changed. Each module moved on its own once it was implemented and validated, so problems could be found and fixed within one area instead of putting the whole product at risk.

A backend that’s easier to evolve

The backend now has clearer responsibilities, consistent patterns, stronger validation and a clean split between HTTP handling and business logic. New features and bug investigations start from code the team can reason about. Together with the separate rebuild of the frontend in React, it completed the move off the inherited PHP stack.