• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » From Monolith to Microservices: A Practical Guide to Migrating Legacy PHP Applications with Laravel, Docker, and AWS Lambda

From Monolith to Microservices: A Practical Guide to Migrating Legacy PHP Applications with Laravel, Docker, and AWS Lambda

Deconstructing the Monolith: Identifying Service Boundaries

The first, and arguably most critical, step in migrating a legacy PHP monolith to a microservices architecture is to identify logical service boundaries. This isn’t about arbitrarily splitting code; it’s about understanding the core business capabilities the application provides. We’re looking for cohesive units of functionality that can operate independently. Common candidates include:

  • User Management (Authentication, Authorization, Profiles)
  • Product Catalog (Inventory, Pricing, Details)
  • Order Processing (Cart, Checkout, Payment Gateway Integration)
  • Notification Service (Email, SMS)
  • Reporting/Analytics

A good heuristic is the “bounded context” concept from Domain-Driven Design. If a module within the monolith has its own distinct set of entities, behaviors, and data, it’s a strong candidate for extraction. Tools like static analysis (e.g., PHPStan with appropriate rulesets) can help identify dependencies and coupling, but human domain expertise is indispensable here. We’ll use a hypothetical e-commerce monolith as our example, focusing on extracting the ‘Product Catalog’ service.

Extracting the Product Catalog Service with Laravel and Docker

Let’s assume our monolith has a monolithic database schema and a set of PHP classes handling product data. We’ll create a new, independent Laravel application for our Product Catalog service. This new application will have its own database schema, ideally a subset of the original, focusing only on product-related data.

First, set up the new Laravel project:

composer create-project laravel/laravel product-catalog-service
cd product-catalog-service

Next, define the database schema for this service. For simplicity, let’s assume a `products` table. We’ll create a migration:

php artisan make:migration create_products_table --create=products

Edit the generated migration file (`database/migrations/YYYY_MM_DD_HHMMSS_create_products_table.php`):

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('sku')->unique();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->integer('stock_quantity')->default(0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Create a `Product` Eloquent model (`app/Models/Product.php`):

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    protected $fillable = [
        'sku',
        'name',
        'description',
        'price',
        'stock_quantity',
    ];

    // Define relationships if any, e.g., to categories, brands, etc.
}

Now, create a controller to handle API requests for products (`app/Http/Controllers/ProductController.php`):

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class ProductController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): JsonResponse
    {
        $products = Product::all();
        return response()->json($products);
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request): JsonResponse
    {
        $validatedData = $request->validate([
            'sku' => 'required|unique:products|max:255',
            'name' => 'required|max:255',
            'description' => 'nullable',
            'price' => 'required|numeric|min:0',
            'stock_quantity' => 'required|integer|min:0',
        ]);

        $product = Product::create($validatedData);

        return response()->json($product, 201);
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id): JsonResponse
    {
        $product = Product::findOrFail($id);
        return response()->json($product);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id): JsonResponse
    {
        $product = Product::findOrFail($id);

        $validatedData = $request->validate([
            'sku' => 'required|max:255|unique:products,sku,' . $product->id,
            'name' => 'required|max:255',
            'description' => 'nullable',
            'price' => 'required|numeric|min:0',
            'stock_quantity' => 'required|integer|min:0',
        ]);

        $product->update($validatedData);

        return response()->json($product);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id): JsonResponse
    {
        $product = Product::findOrFail($id);
        $product->delete();

        return response()->json(null, 204);
    }
}

Define the API routes in `routes/api.php`:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::apiResource('products', ProductController::class);

To containerize this service, create a `Dockerfile` in the project root:

FROM php:8.2-fpm

WORKDIR /var/www/html

COPY . .

RUN apt-get update && apt-get install -y \
    git \
    curl \
    libzip-dev \
    unzip \
    && docker-php-ext-install zip \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

RUN curl -sSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

RUN composer install --no-dev --optimize-autoloader

EXPOSE 9000
CMD ["php-fpm"]

And a `docker-compose.yml` for local development:

version: '3.8'

services:
    app:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - "9000:9000"
        volumes:
            - .:/var/www/html
        depends_on:
            - db

    db:
        image: mysql:8.0
        ports:
            - "3306:3306"
        volumes:
            - db_data:/var/lib/mysql
        environment:
            MYSQL_ROOT_PASSWORD: rootpassword
            MYSQL_DATABASE: product_catalog_db
            MYSQL_USER: user
            MYSQL_PASSWORD: password
        networks:
            - app-network

networks:
    app-network:
        driver: bridge

volumes:
    db_data:

Configure your `.env` file for the database connection:

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=product_catalog_db
DB_USERNAME=user
DB_PASSWORD=password

Run the migrations:

docker-compose up -d
docker-compose exec app php artisan migrate

Data Synchronization Strategy

Extracting the service is only half the battle; we need to ensure data consistency between the monolith and the new microservice. Several strategies exist:

  • One-Time Data Migration: Suitable for initial setup or if the monolith’s product data is relatively static. This involves a script to export data from the monolith’s database and import it into the new service’s database.
  • Dual Writes: When a product is updated in the monolith, it’s also sent to the new service’s API. This requires modifying the monolith’s code.
  • Event-Driven Synchronization: The monolith publishes events (e.g., `ProductCreated`, `ProductUpdated`) to a message queue (like RabbitMQ or Kafka). The microservice subscribes to these events and updates its own database. This is the most robust and scalable approach for ongoing synchronization.
  • Change Data Capture (CDC): Tools like Debezium can capture database changes from the monolith’s database and stream them to a message queue, which the microservice consumes. This decouples the synchronization from the monolith’s application code.

For our example, let’s outline an event-driven approach. We’ll assume the monolith can be modified to publish events. If not, CDC is the next best option.

Monolith Modification (Conceptual – PHP):

// In the monolith's Product model or repository after a save/update operation
use App\Events\ProductUpdated; // Assuming an event system exists

// ... after $product->save(); or $product->update([...]);
event(new ProductUpdated($product->toArray()));

Microservice Event Listener (Laravel):

// Create an event listener for product updates
// php artisan make:listener ProcessProductUpdatedEvent --event=ProductUpdated

// In app/Listeners/ProcessProductUpdatedEvent.php
namespace App\Listeners;

use App\Events\ProductUpdated; // Assuming this event is defined in the microservice
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class ProcessProductUpdatedEvent implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * Handle the event.
     */
    public function handle(ProductUpdated $event): void
    {
        $productData = $event->productData; // Assuming event passes an array

        // Find or create the product in the microservice's database
        Product::updateOrCreate(
            ['sku' => $productData['sku']], // Unique identifier
            [
                'name' => $productData['name'],
                'description' => $productData['description'] ?? null,
                'price' => $productData['price'],
                'stock_quantity' => $productData['stock_quantity'],
                // Timestamps will be handled by Eloquent if not provided
            ]
        );
    }
}

You would need to configure your Laravel application to use a queue driver (e.g., Redis, SQS) and ensure the monolith is publishing events to a compatible message broker that the microservice can consume.

Integrating with AWS Lambda and API Gateway

For serverless deployment and cost-efficiency, we can deploy our Product Catalog service to AWS Lambda. This requires adapting the Laravel application to run within the Lambda environment. The popular Bref project is an excellent tool for this.

1. Install Bref:**

composer require bref/bref bref/laravel-bridge

2. Configure `serverless.yml`:**

service: product-catalog-service

provider:
    name: aws
    runtime: php8.2 # Match your PHP version
    region: us-east-1 # Your preferred AWS region
    memorySize: 512 # Adjust as needed
    timeout: 30 # Adjust as needed
    environment:
        APP_ENV: production
        APP_URL: ${env:APP_URL, 'https://your-api-gateway-url.execute-api.us-east-1.amazonaws.com/prod'} # Set your API Gateway URL
        APP_KEY: base64:YOUR_APP_KEY_HERE # Generate with php artisan key:generate --show
        DB_CONNECTION: mysql
        DB_HOST: your-rds-endpoint.rds.amazonaws.com # Your RDS endpoint
        DB_PORT: 3306
        DB_DATABASE: product_catalog_db
        DB_USERNAME: your_db_user
        DB_PASSWORD: your_db_password
    iam:
        role:
            statements:
                - Effect: Allow
                  Action:
                      - rds-data:ExecuteStatement
                      - rds-data:BatchExecuteStatement
                  Resource: "arn:aws:rds-data:us-east-1:YOUR_ACCOUNT_ID:cluster:YOUR_RDS_CLUSTER_IDENTIFIER" # Or specific DB ARN

plugins:
    - serverless-php-build
    - serverless-dotenv-plugin

package:
    individually: true
    patterns:
        - '!node_modules/**'
        - '!vendor/bref/php-http-client/**' # Exclude if not needed for Lambda runtime
        - '!vendor/laravel/sail/**' # Exclude development tools

functions:
    api:
        handler: public/index.php # Bref's entry point for Laravel
        description: Handles all API requests for the product catalog
        events:
            - httpApi:
                path: /
                method: ANY
            - httpApi:
                path: /{proxy+}
                method: ANY
        # Optional: Configure VPC access if your RDS instance is in a private subnet
        # vpc:
        #     securityGroupIds:
        #         - sg-xxxxxxxxxxxxxxxxx
        #     subnetIds:
        #         - subnet-xxxxxxxxxxxxxxxxx
        #         - subnet-yyyyyyyyyyyyyyyyy

# Optional: Configure RDS Proxy for better Lambda-RDS connectivity
# custom:
#     serverless-php-build:
#         php:
#             version: 8.2
#             extensions:
#                 - pdo_mysql
#                 - json
#                 - mbstring
#                 - xml
#     dotenv:
#         basePath: ./
#         file: .env

Important Notes for `serverless.yml`:**

  • Replace placeholders like `YOUR_ACCOUNT_ID`, `YOUR_RDS_CLUSTER_IDENTIFIER`, `your-rds-endpoint.rds.amazonaws.com`, `your_db_user`, `your_db_password`, and `YOUR_APP_KEY_HERE` with your actual AWS credentials and generated app key.
  • The `iam.role.statements` grants permissions to interact with RDS Data API. If you’re not using RDS Proxy, you’ll need to configure network access (VPC settings) and potentially use a different database driver.
  • `serverless-php-build` is crucial for compiling PHP extensions needed by Laravel and your application.
  • `serverless-dotenv-plugin` helps manage environment variables.
  • The `package.patterns` are important to keep the Lambda deployment package size small.

3. Deploy to AWS:**

# Install Serverless Framework if you haven't already
npm install -g serverless

# Deploy the service
sls deploy

After deployment, Serverless Framework will output the API Gateway endpoint URL. You’ll need to configure your monolith (or other services) to call this endpoint for product-related data.

Refactoring the Monolith to Consume Microservice APIs

Once the Product Catalog service is deployed and accessible via API Gateway, the monolith needs to be refactored to consume its endpoints instead of accessing the database directly for product information. This is a gradual process.

1. Create an API Client (Laravel Monolith or a dedicated client library):

// In the monolith's application (assuming it's also Laravel for consistency)
// app/Services/ProductApiClient.php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class ProductApiClient
{
    protected string $baseUrl;

    public function __construct()
    {
        $this->baseUrl = env('PRODUCT_CATALOG_API_URL'); // e.g., https://your-api-gateway-url.execute-api.us-east-1.amazonaws.com/prod
    }

    public function getProductBySku(string $sku): ?array
    {
        try {
            $response = Http::get("{$this->baseUrl}/products", ['sku' => $sku]); // Assuming an endpoint to filter by SKU
            if ($response->successful() && !empty($response->json())) {
                return collect($response->json())->first(); // Get the first product if multiple are returned
            }
            return null;
        } catch (\Exception $e) {
            // Log error
            report($e);
            return null;
        }
    }

    public function getProductById(string $id): ?array
    {
        try {
            $response = Http::get("{$this->baseUrl}/products/{$id}");
            if ($response->successful()) {
                return $response->json();
            }
            return null;
        } catch (\Exception $e) {
            // Log error
            report($e);
            return null;
        }
    }

    // Add methods for other product operations (e.g., updateStock)
}

Configure the API URL in the monolith’s `.env` file:

PRODUCT_CATALOG_API_URL=https://your-api-gateway-url.execute-api.us-east-1.amazonaws.com/prod

2. Replace Direct Database Access:**

// In a relevant part of the monolith's code (e.g., a controller or service)

// Instead of:
// $product = \App\Models\MonolithProduct::where('sku', $sku)->first();

// Use the API client:
use App\Services\ProductApiClient;

// ... inside a method
$productApiClient = new ProductApiClient();
$productData = $productApiClient->getProductBySku($sku);

if ($productData) {
    // Use $productData to display information or perform actions
    $productName = $productData['name'];
    $productPrice = $productData['price'];
    // ...
} else {
    // Handle product not found
}

This refactoring should be done incrementally. Start by replacing read operations, then move to write operations (like updating stock), ensuring that the data synchronization strategy is robust enough to handle potential race conditions or inconsistencies during the transition.

Conclusion and Next Steps

Migrating from a monolith to microservices is a significant undertaking. This guide provides a practical, code-centric approach to extracting a Product Catalog service from a legacy PHP application using Laravel, Docker, and deploying it serverlessly on AWS Lambda. Key considerations include identifying service boundaries, managing data synchronization, and refactoring the monolith to consume new APIs.

Future steps would involve extracting other services (User Management, Order Processing), establishing robust inter-service communication patterns (e.g., using gRPC or asynchronous messaging), implementing comprehensive monitoring and logging across all services, and refining deployment pipelines for a fully automated CI/CD process.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • From Monolith to Microservices: A Practical Guide to Migrating Legacy PHP Applications with Laravel, Docker, and AWS Lambda
  • Leveraging AWS Graviton with Dockerized Laravel for High-Performance, Cost-Optimized Web Applications
  • Migrating Legacy PHP 7.x Applications to PHP 9 with Zero Downtime: A Deep Dive into Strangler Fig Pattern and Docker Orchestration
  • Orchestrating Microservices with PHP 8/9 and Laravel: A Deep Dive into Docker Swarm and AWS ECS
  • Leveraging PHP 8 JIT and Swoole for High-Performance, Event-Driven Laravel Applications on AWS Lambda

Categories

  • apache (1)
  • AWS (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (664)
  • Desktop Applications (14)
  • DevOps (72)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (76)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • Performance & Security Optimization (8)
  • PHP (261)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (10)
  • Python (20)
  • Ruby on Rails (1)
  • Security (1)
  • Security & Compliance (650)
  • SEO & Growth (492)
  • Server (118)
  • Softwares (1)
  • Ubuntu (9)
  • Uncategorized (517)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (136)
  • WordPress Plugin Development (728)
  • WordPress Theme Development (357)

Recent Posts

  • From Monolith to Microservices: A Practical Guide to Migrating Legacy PHP Applications with Laravel, Docker, and AWS Lambda
  • Leveraging AWS Graviton with Dockerized Laravel for High-Performance, Cost-Optimized Web Applications
  • Migrating Legacy PHP 7.x Applications to PHP 9 with Zero Downtime: A Deep Dive into Strangler Fig Pattern and Docker Orchestration

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (728)
  • Debugging & Troubleshooting (664)
  • Security & Compliance (650)
  • Uncategorized (517)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala