Hasina Razafintsalama

Hasina RAZAFINTSALAMA

← Back to Blog
Architecture

Migrating a Laravel Monolith to Microservices

A step-by-step approach to breaking up a Laravel monolith without bringing production down: when not to, the prerequisites, the strangler fig pattern, finding boundaries, and the hard part, the data.

2026-06-10·12 min

Migrating a monolith to microservices is one of the most complex decisions a tech team can make. Done well, it unlocks independent scaling and team autonomy. Done poorly, it creates a distributed monolith with all the operational cost of microservices and none of the benefits. This is how to do it incrementally, and how to know whether you should.

First, decide whether to migrate at all

Most applications do not need microservices. The problems people blame on the monolith, slow deploys, tangled code, teams stepping on each other, are usually solved by a modular monolith: clear module boundaries, enforced dependencies, one deployable. Move to services when a specific part needs to scale or deploy independently, or when teams genuinely cannot work without blocking each other, not because the architecture is fashionable.

OptionDeployDataGood when
MonolithOne unitOne databaseSmall team, early product, unclear domain
Modular monolithOne unitOne database, module-owned schemasMost teams; clear modules, no ops overhead
MicroservicesPer serviceDatabase per serviceParts scale or deploy independently, many teams

Prerequisites before you extract anything

  • Distributed tracing and centralized logs; you cannot debug a request across services without them.
  • Automated CI/CD per service, so a new service is not a manual deploy.
  • A test suite you trust on the monolith, so you can tell whether an extraction broke something.
  • A clear read on your domain boundaries; if you cannot name your bounded contexts, you are not ready.

The strangler fig pattern

Instead of a big-bang rewrite, you route traffic through a facade and gradually redirect slices of it from the monolith to new services. The monolith keeps running and stays the fallback. Over time it shrinks until what is left is either retired or is itself just another service.

text
        client
          |
      [ gateway / facade ]
        /            \
  monolith        new service
  (shrinking)     (growing, one context at a time)

Never attempt a full rewrite of a running monolith. Extract one bounded context at a time, keep the monolith as the fallback, and migrate traffic gradually behind a feature flag so you can roll back in seconds.

Find the boundaries with DDD

The hardest part is not the code, it is choosing where to cut. Use Domain-Driven Design to identify bounded contexts, and an event storming session with the people who know the domain to surface them. Extract the least-coupled context first: it teaches you the mechanics with the lowest risk.

  • Map the domain: aggregates, bounded contexts, and the events that cross between them.
  • Spot the pain: which modules always deploy together, which slow each other down, which one team is blocked on.
  • Decide data ownership: each service owns its data, and no other service reads its tables directly.
  • Write the API and event contracts before a line of service code.

Practical steps with Laravel

Extract the module inside the monolith first (its own namespace, an interface at the boundary), put an anti-corruption layer in front of it so the rest of the monolith talks to an interface, then move the implementation behind HTTP or events, and cut over with a flag.

php
// Before: direct coupling inside the monolith
class OrderController extends Controller {
    public function store(Request $request, UserService $users) {
        $user = $users->find($request->user_id);
        // ...
    }
}

// After: the boundary is an interface; the implementation calls the service
interface UserDirectory {
    public function find(int $id): ?UserData;
}

class HttpUserDirectory implements UserDirectory {
    public function find(int $id): ?UserData {
        $data = Http::get(config('services.users.url') . "/users/{$id}")->json();
        return $data ? UserData::fromArray($data) : null;
    }
}

The hard part: the data

Splitting the code is straightforward; splitting the database is where migrations stall. Do it in stages: first give each context its own schema inside the shared database and stop cross-schema joins, then move the new service to its own database. During the transition, keep the two in sync with an outbox table or change data capture rather than dual writes from application code, which are hard to make reliable.

Communication: synchronous or asynchronous

Use synchronous HTTP or gRPC only when the caller needs an immediate answer: authentication, a payment authorization. Use asynchronous events (RabbitMQ, Kafka) for everything that can happen eventually: notifications, audit logs, analytics, and cross-service updates. A chain of synchronous calls where A waits on B waits on C compounds latency and failure at every hop.

The failure mode: a distributed monolith

If you cannot deploy one service without redeploying others, or a bug in one takes down features that should be unrelated, you have microservices in name and a monolith in practice, now with network calls in the middle. The usual causes are a shared database and synchronous call chains. Both are worth stopping and fixing before extracting the next service.

FAQ

When should I migrate a Laravel monolith to microservices?
When a specific part of the system needs to scale or deploy independently, or when multiple teams genuinely block each other in one codebase. If the pain is slow deploys or tangled code, a modular monolith fixes that without the operational cost. Do not migrate because the term is fashionable.
What is the strangler fig pattern?
A migration strategy where you route traffic through a facade and gradually redirect slices of it from the monolith to new services, one bounded context at a time. The monolith keeps running as the fallback and shrinks over time, so there is never a big-bang cutover.
How do I split the database of a monolith?
In stages. First give each bounded context its own schema in the shared database and stop cross-schema joins. Then move the new service to its own database, keeping the two in sync during the transition with an outbox table or change data capture rather than application-level dual writes.
How long does a microservices migration take?
Months, not weeks, and it scales with the number of bounded contexts and how entangled the data is. Extract one context, let it run in production, consolidate, then do the next. Teams that rush the first few extractions usually end up with a distributed monolith.
Do I have to migrate everything?
No. Extract the parts that need independent scaling or deployment and leave the rest in the monolith. A stable core monolith surrounded by a few services is a common and healthy end state; full decomposition is rarely worth it.

A microservices migration is a journey measured in months. Pace yourself, measure the outcome of each extraction against the reason you started, and do not hesitate to pause and consolidate before the next one.

Need help with this topic? Microservices Migration

Discover this service