Home/ Engineering Insights/ Monolith to Microservices Transformation

Converting Legacy Monoliths to Microservices: Strategy & Risk

The request usually starts the same way: "our PHP monolith is holding us back, we need microservices." Half the time, what's actually holding the team back is a monolith with no test coverage and a deploy process that requires everyone to hold hands and pray — problems microservices don't fix, and can actively make worse by spreading the same untested code across a distributed system with network calls in between.

The question we ask first: is there a specific feature that genuinely needs independent scaling or independent deployment, or is the real complaint "our codebase is a mess"? Only the first one is a microservices problem. The second is a modularization and testing problem, and it's cheaper to fix inside the monolith.

What's actually tangled in a PHP monolith

The obstacle to extraction is almost never the framework — it's the shared MySQL schema and the implicit coupling that grows in any codebase over years. In a typical Laravel or Symfony monolith we audit, the tangle shows up as Eloquent models or Doctrine entities from unrelated feature areas joining against the same tables directly, application-wide cache keys and session state multiple features read and write without realizing it, and event listeners registered globally that silently depend on execution order.

Before extracting anything, we map this by tracing actual data flow rather than trusting the folder structure — a "billing" module in the codebase often turns out to read from `users`, `subscriptions`, and three other tables owned conceptually by other features, and every one of those reads is a coupling point that has to be resolved before billing can become an independent service.

The strangler pattern, in PHP terms

We don't recommend a rewrite. We recommend extracting one isolated feature at a time while the monolith keeps serving everything else — put a thin router (nginx location blocks or a small Kong/Traefik gateway) in front of both, send traffic for the extracted feature to the new service, and leave a fallback path to the monolith code until the new service has proven itself under real load. Concretely, that first extraction is usually a service exposing a small REST API, backed by its own database, that the monolith calls instead of hitting its own tables directly for that feature going forward.

Pick the first candidate for how isolated and low-traffic it is, not how important it is. A notification or reporting feature with few cross-table joins is a better first extraction than the checkout flow, even though checkout is the one everyone wants scaled — you want to learn the operational pattern (deployment, monitoring, on-call runbooks) on something that won't take the business down if it goes wrong.

Communication: start synchronous, add a queue later

Most PHP shops already have a message broker available or easy to add — RabbitMQ via php-amqplib, or Laravel's own queue abstraction backed by Redis or SQS — but we advise against reaching for async messaging on day one. Start with a direct REST call from the monolith to the new service; it's easier to debug, and you can see exactly where the coupling still hurts. Move specific calls to a queue only once you've identified an actual case where the monolith shouldn't block waiting on the new service (bulk notification sends, report generation, image processing are common candidates).

The database split is the hard part, not the code

Extracting the PHP code that implements a feature is usually a week or two of focused work. Giving that feature its own database, so it's actually independent instead of a service that still reads the monolith's MySQL instance directly, is what takes months. The sequence that works in practice: first let the new service read from the shared database (not ideal, but functional), then have the monolith publish an event whenever it writes data the new service cares about, then migrate the underlying tables to the service's own schema and have it consume events instead of querying the shared DB directly. Skipping straight to "give it its own database" without this staged approach is where most PHP microservices migrations we've been called in to rescue actually stalled.

What it costs beyond engineering time

Phase Timeline What actually happens
Assessment 2-4 weeks Trace real data flow across tables, not folder structure; pick the first extraction candidate
Infrastructure setup 4-8 weeks Gateway/routing layer, container deployment, centralized logging so a request can be traced across services
First extraction 6-10 weeks Build the service, stage the database split, prove the fallback path actually works under failure
Subsequent services 4-8 weeks each Faster once the gateway and deployment pattern exist — the coupling analysis is still the long pole each time

For a mid-size monolith with 10-20 distinct feature areas, total elapsed time typically lands at 6-12 months for a handful of extracted services, not a full distributed rewrite. Trying to convert everything is where the 12-24 month numbers come from, and it's rarely worth pushing that far — most platforms only actually need two or three features pulled out.

Where extraction should stop

Not every feature belongs outside the monolith. Anything requiring multi-table transactions that must succeed or fail together stays put — distributed transactions across services are a real engineering cost most teams shouldn't take on for a feature that doesn't need independent scaling. Low-traffic internal tools are rarely worth the operational overhead of a separate deployment either. The honest goal isn't "microservices everywhere," it's isolating the two or three things that are genuinely constrained by living inside the monolith.

Not sure if your monolith actually needs to become microservices?

We'll trace the real coupling in your codebase and tell you honestly whether extraction is worth it — or whether the fix is inside the monolith.

Get Architecture Consultation