0

Monolith to microservices — when it's worth it, how to start

Advanced5 min read·eng-11-009
interview

Concept

Monolith to microservices — this is eng-05-008 expanded with the HOW, not just the WHEN.

When it's worth it (repeat the decision framework):

  • Monolith is hitting a genuine bottleneck that can't be solved by optimization or vertical scaling.
  • You have multiple independent teams who need to deploy independently.
  • Specific parts of the system have drastically different scaling requirements.
  • You've proven your domain model is stable enough to not change the service boundary.

The modular monolith as the intermediate step: Before splitting into separate services, modularize the monolith. Create clear module boundaries. If modules only communicate via well-defined interfaces, extraction becomes a configuration change.

Strangler Fig Pattern: Don't rewrite — strangle. Route new traffic to the new service while the old monolith handles the rest. Gradually move functionality from the monolith to the microservice. The monolith "dies" over time as its responsibilities are strangled away.

Data migration: The hardest part. Each microservice must own its own data. Options:

  1. Start by having the service call the monolith's database (temporary, but functional).
  2. Sync data to the new service's database via events.
  3. Cut over: new service reads/writes its own DB. Monolith stops owning that data.

Service communication:

  • Synchronous: HTTP/REST or gRPC. Simple but creates coupling (if service B is down, A's request fails).
  • Asynchronous: Message queue (RabbitMQ, Kafka, SQS). Services are decoupled. Retry on failure. But eventual consistency.

In Laravel: A "microservice" is typically another Laravel app. Share code via Composer packages. Communicate via HTTP (Http:: facade) or queues.

Code Example

php
<?php
// ============================================================
// STEP 1: Modular monolith — clear internal boundaries
// ============================================================
// All in one Laravel app, but with strict module boundaries

namespace App\Modules\Inventory;

interface InventoryPort
{
    public function isAvailable(string $productId, int $qty): bool;
    public function reserve(string $productId, int $qty): void;
}

class EloquentInventoryAdapter implements InventoryPort
{
    public function isAvailable(string $productId, int $qty): bool
    {
        return Product::where('id', $productId)->where('stock', '>=', $qty)->exists();
    }
    public function reserve(string $productId, int $qty): void
    {
        Product::where('id', $productId)->decrement('stock', $qty);
    }
}

// ============================================================
// STEP 2: Strangler Fig — extract Inventory to its own service
// ============================================================
// New service: https://inventory.internal/api
// Replace adapter WITHOUT changing OrderService (it depends on InventoryPort)

namespace App\Modules\Inventory;

class HttpInventoryAdapter implements InventoryPort
{
    public function __construct(private readonly \Illuminate\Http\Client\PendingRequest $http) {}

    public function isAvailable(string $productId, int $qty): bool
    {
        return $this->http->get("/products/{$productId}/availability", ['qty' => $qty])
            ->throw()
            ->json('available');
    }

    public function reserve(string $productId, int $qty): void
    {
        $this->http->post("/products/{$productId}/reserve", ['qty' => $qty])->throw();
    }
}

// ServiceProvider — swap which adapter is used via config
if (config('services.inventory.mode') === 'http') {
    app()->bind(InventoryPort::class, fn() => new HttpInventoryAdapter(
        \Http::baseUrl(config('services.inventory.url'))
            ->withToken(config('services.inventory.token'))
            ->timeout(5)
            ->retry(2, 200)
    ));
} else {
    app()->bind(InventoryPort::class, EloquentInventoryAdapter::class);
}

// ============================================================
// STEP 3: Async communication via events (decoupled)
// ============================================================
// Instead of HTTP calls (synchronous coupling), emit an event:
class OrderPlaced implements \Illuminate\Contracts\Broadcasting\ShouldBroadcast
{
    public function __construct(public readonly int $orderId, public readonly array $items) {}
}

// Inventory service subscribes to OrderPlaced event from the message queue
// If inventory service is down: event stays in queue, processed when it comes back up