Kembali ke Beranda Blog
Software Architecture 5 min read 21 pembaca

Microservices Architecture with Modern PHP & Docker

An authoritative, hands-on masterclass guiding backend engineers through designing, building, and deploying scalable microservices using modern PHP 8, Docker containers, API Gateways, and event-driven messaging pipelines.

Dahlan

Dahlan

Software Architect • 24 Agustus 2026

Microservices Architecture with Modern PHP & Docker

Microservices Architecture: Building Decoupled Systems with Modern PHP & Docker

Chapter 1. Monolith vs. Microservices: Strategic Architectural Evolution

Deciding when to transition from a monolithic architecture to distributed microservices is a fundamental technical strategy decision. A monolithic application unifies business domain logic, data persistence, user interfaces, and background workers into a single codebase and deployment unit. While monoliths excel in early-stage product development due to simple local testing and straightforward deployments, enterprise scale introduces friction points across engineering organizations.

The Monolithic Scaling Wall

As engineering teams expand and traffic scales, monolithic applications present several architectural limitations:

  • Deployment Coupling: A tiny code modification in a non-critical module requires rebuilding, testing, and deploying the entire application stack.

  • Database Contention: Multiple domain entities contend for locks and connection pools on a single database server, creating severe I/O bottlenecks.

  • Scaling Inefficiencies: Scaling a monolith requires replicating the whole stack across heavier virtual machines, even if only a single background endpoint experiences high traffic load.

  • Organizational Friction: Dozens of software developers modifying a single codebase leads to frequent code merge conflicts, complex continuous integration pipelines, and regression risks.

The Microservices Solution

A microservices architecture addresses these challenges by decomposing the application into autonomous, single-responsibility services. Each microservice manages its own domain logic, persistence layer, and deployment lifecycle, communicating with sibling services exclusively through explicit network interfaces (REST, gRPC, or asynchronous event brokers).

Through strict domain boundaries, microservices allow engineering teams to deploy changes independently, scale high-demand services horizontally on lightweight cloud instances, and isolate application failures before they cascade across the distributed system.

Chapter 2. Defining Domain Boundaries & Database-per-Service Principles

The primary cause of microservices migration failure is poor service decomposition. Creating services based on arbitrary visual interfaces or shallow code abstractions results in a "distributed monolith"—a system that suffers from the network latency and operational complexity of microservices without delivering any deployment autonomy or scalability benefits.

Strategic Domain-Driven Design (DDD)

Domain-Driven Design (DDD) provides the formal methodology for identifying clean microservice boundaries using Bounded Contexts. A Bounded Context defines the explicit boundary within which a domain model applies. For instance, in an enterprise e-commerce system:

  1. Ordering Context: Handles cart checkouts, order line items, and invoice generation.

  2. Inventory Context: Tracks warehouse stock levels, supplier shipments, and SKU availability.

  3. Notification Context: Manages transactional emails, SMS dispatch, and push notification templates.

Rather than sharing a global User or Product database model across the entire application, each microservice defines its own minimal model tailored specifically to its business responsibilities.

The Database-per-Service Pattern

To guarantee true decoupling, every microservice must strictly own its persistence layer. Under no circumstances should one microservice query or write directly to the database instance of another service.

[ Ordering Service ]        [ Inventory Service ]        [ Payment Service ]
        │                            │                            │
        ▼                            ▼                            ▼
( Order PostgreSQL )        ( Inventory MySQL )          ( Payment Redis DB )

Direct database sharing introduces tight coupling at the data schema level. When Service A depends on Service B's database tables, schema migrations in Service B risk breaking Service A without warning. Enforcing the Database-per-Service pattern guarantees that data schema modifications remain completely internal to the owning microservice, preserving clear API contracts across application boundaries.

Chapter 3. Communication Protocols: Synchronous REST vs. gRPC vs. Asynchronous Messaging

In a distributed microservices ecosystem, services must continuously exchange data. Choosing the correct communication protocol depends directly on the latency, throughput, and consistency requirements of each interaction.

Synchronous Communication: REST vs. gRPC

Synchronous protocols require the calling client service to wait for the target service to process the request and return an HTTP response.

  • REST over HTTP/1.1: Utilizes JSON payloads over standard HTTP methods (GET, POST, PUT, DELETE). It is human-readable, universally supported, and ideal for external-facing client APIs. However, JSON parsing and HTTP/1.1 text serialization introduce noticeable CPU and network overhead in internal service-to-service calls.

  • gRPC over HTTP/2: Utilizes Protocol Buffers (Protobuf)—a compact, binary serialization format—transported over HTTP/2 multiplexed streams. gRPC delivers significantly lower latency, smaller payload sizes, and strongly typed contract generation, making it the preferred standard for internal, service-to-service synchronous communication.

Asynchronous Communication: Event Brokers

Asynchronous protocols decouple the producer service from the consumer service completely. The producing service emits a domain event to a centralized broker (such as RabbitMQ or Apache Kafka) and immediately resumes its execution without waiting for downstream processing. Asynchronous messaging is essential for high-throughput background processing, cross-service notifications, and eventual consistency models.

Chapter 4. API Gateway Pattern: Centralized Routing, Rate Limiting & Auth

Exposing dozens of individual microservice IP addresses directly to client applications (web frontends, mobile apps, third-party integrations) creates security risks and client-side complexity. The API Gateway Pattern introduces a reverse proxy layer that serves as the single entry point for all external client traffic.

Responsibilities of the API Gateway

Below is a production-ready, lightweight API Gateway implementation built in modern PHP utilizing strict typing, modular request forwarding, and JWT token validation primitives.

  • Request Routing: Dynamically inspects incoming HTTP request paths and proxies them to the correct internal microservice cluster.

  • Authentication & Authorization: Validates client API keys or JWT (JSON Web Token) signatures once at the edge before forwarding authenticated requests downstream.

  • Rate Limiting & Throttling: Enforces request quotas per IP or API key to protect internal infrastructure from denial-of-service traffic spikes.

  • Protocol Translation: Converts external REST/JSON client requests into efficient internal gRPC binary calls.

<?php

declare(strict_types=1);

namespace App\Gateway;

use RuntimeException;
use InvalidArgumentException;

final class ApiGateway
{
    /**
     * @param array<string, string> $serviceRoutes Path prefix to internal service host mapping
     */
    public function __construct(
        private array $serviceRoutes,
        private string $jwtSecretKey
    ) {}

    public function handleIncomingRequest(string $requestUri, string $method, array $headers, string $body): void
    {
        $path = parse_url($requestUri, PHP_URL_PATH) ?? '/';
        
        // 1. Authenticate Request
        $this->authenticateBearerToken($headers);

        // 2. Resolve Downstream Target Service
        $targetHost = $this->resolveTargetService($path);

        // 3. Proxy Request Internal Service
        $this->proxyRequest($targetHost, $path, $method, $headers, $body);
    }

    private function authenticateBearerToken(array $headers): void
    {
        $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? null;
        
        if ($authHeader === null || !str_starts_with($authHeader, 'Bearer ')) {
            $this->sendJsonResponse(['error' => 'Unauthorized: Missing or malformed token.'], 401);
            exit;
        }

        $jwt = substr($authHeader, 7);
        if (!$this->verifyJwtSignature($jwt)) {
            $this->sendJsonResponse(['error' => 'Unauthorized: Invalid token signature.'], 403);
            exit;
        }
    }

    private function resolveTargetService(string $path): string
    {
        foreach ($this->serviceRoutes as $prefix => $host) {
            if (str_starts_with($path, $prefix)) {
                return $host;
            }
        }

        $this->sendJsonResponse(['error' => 'Route not found on API Gateway.'], 404);
        exit;
    }

    private function proxyRequest(string $targetHost, string $path, string $method, array $headers, string $body): void
    {
        $targetUrl = rtrim($targetHost, '/') . $path;
        
        $ch = curl_init($targetUrl);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        
        $formattedHeaders = [];
        foreach ($headers as $key => $value) {
            $formattedHeaders[] = "{$key}: {$value}";
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders);

        $response = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        http_response_code($statusCode);
        echo $response;
    }

    private function verifyJwtSignature(string $jwt): bool
    {
        $parts = explode('.', $jwt);
        if (count($parts) !== 3) {
            return false;
        }

        [$headerB64, $payloadB64, $sigB64] = $parts;
        $expectedSignature = hash_hmac('sha256', "{$headerB64}.{$payloadB64}", $this->jwtSecretKey, true);
        $providedSignature = $this->base64UrlDecode($sigB64);

        return hash_equals($expectedSignature, $providedSignature);
    }

    private function base64UrlDecode(string $data): string
    {
        $remainder = strlen($data) % 4;
        if ($remainder) {
            $data .= str_repeat('=', 4 - $remainder);
        }
        return base64_decode(strtr($data, '-_', '+/'));
    }

    private function sendJsonResponse(array $data, int $statusCode): void
    {
        http_response_code($statusCode);
        header('Content-Type: application/json');
        echo json_encode($data);
    }
}

Chapter 5. Asynchronous Event Messaging with RabbitMQ AMQP

When a customer submits an order in an e-commerce platform, synchronously waiting for inventory updates, payment processing, email notifications, and analytics logging creates severe response latency. Using an asynchronous message queue allows the Order Service to record the purchase, publish an OrderPlaced event to RabbitMQ, and immediately return a success response to the customer.

Production Order Publisher Implementation

Below is a modern PHP service that connects to RabbitMQ via AMQP, initializes resilient exchange routing, and publishes strongly typed JSON event payloads.

<?php

declare(strict_types=1);

namespace App\Messaging;

use AMQPConnection;
use AMQPChannel;
use AMQPExchange;
use Exception;

final class OrderEventPublisher
{
    private AMQPConnection $connection;
    private AMQPChannel $channel;
    private AMQPExchange $exchange;

    public function __construct(
        string $host = '127.0.0.1',
        int $port = 5672,
        string $user = 'guest',
        string $password = 'guest'
    ) {
        $this->connection = new AMQPConnection([
            'host'     => $host,
            'port'     => $port,
            'login'    => $user,
            'password' => $password,
        ]);

        $this->connection->connect();
        $this->channel = new AMQPChannel($this->connection);
        
        $this->exchange = new AMQPExchange($this->channel);
        $this->exchange->setName('domain_events');
        $this->exchange->setType(AMQP_EXETYPE_TOPIC);
        $this->exchange->setFlags(AMQP_DURABLE);
        $this->exchange->declareExchange();
    }

    public function publishOrderPlacedEvent(string $orderId, int $customerId, float $amount): void
    {
        $payload = json_encode([
            'event_id'     => bin2hex(random_bytes(16)),
            'event_type'   => 'order.placed',
            'order_id'     => $orderId,
            'customer_id'  => $customerId,
            'total_amount' => $amount,
            'occurred_at'  => date(DATE_ATOM),
        ], JSON_THROW_ON_ERROR);

        $routingKey = 'events.orders.placed';

        $this->exchange->publish(
            $payload,
            $routingKey,
            AMQP_NOPARAM,
            [
                'content_type'  => 'application/json',
                'delivery_mode' => 2, // Persistent message storage on disk
            ]
        );
    }

    public function __destruct()
    {
        if ($this->connection->isConnected()) {
            $this->connection->disconnect();
        }
    }
}

Chapter 6. Distributed Data Consistency & Resilience: The Saga Pattern & Circuit Breakers

In a monolithic architecture, data consistency is guaranteed by database ACID transactions (BEGIN ... COMMIT). If a step fails during checkout, the entire SQL transaction rolls back safely. In microservices, where each service owns its isolated database, traditional ACID transactions across network boundaries are impossible.

The Saga Pattern for Eventual Consistency

The Saga Pattern manages distributed transactions as a sequence of localized microservice transactions. Each local transaction updates its database and emits an event triggering the next transaction in sibling services. If a local transaction fails (for instance, payment card declined), the Saga coordinator issues a series of compensating transactions that execute in reverse order to roll back previous state modifications.

[ Order Created ] ──> [ Payment Processed ] ──> [ Inventory Reserved ]
       │                      │                         │
       ▼                      ▼                         ▼
( Success )              ( FAILED! ) ──────────> ( Execute Compensating
                                                  Inventory Release )

Circuit Breaker Pattern for Fault Tolerance

When an internal microservice experiences high latency or system crashes, sibling services making synchronous network calls to it risk exhausting their own worker threads while waiting for socket timeouts. The Circuit Breaker Pattern wraps network calls in a protective state monitor.

  1. Closed State: Requests pass through normally. Failures are counted.

  2. Open State: If failure rates cross a configured threshold, the circuit trips open instantly. All subsequent calls fail immediately without attempting network execution, returning fallback data.

  3. Half-Open State: After a timeout period, the circuit allows a small test batch of requests through. If successful, it resets to Closed; if failures persist, it returns to Open.

Chapter 7. Container Orchestration with Docker Compose

To run a microservices ecosystem locally or in staging, every microservice and its supporting infrastructure dependencies (databases, brokers, reverse proxies) must be containerized. Docker provides lightweight, reproducible runtimes, while Docker Compose orchestrates multi-container environments using a declarative configuration file.

Complete Multi-Service Docker Architecture (docker-compose.yml)

The following production-grade docker-compose.yml file configures an isolated API Gateway, an Order Microservice running PHP 8.3-FPM, a dedicated MySQL database instance, a RabbitMQ message broker with management UI, and persistent volume networks.

version: '3.8'

services:
  # 1. Reverse Proxy & API Gateway Layer
  api-gateway:
    image: nginx:alpine
    container_name: microservices_gateway
    ports:
      - "8080:80"
    volumes:
      - ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - order-service
    networks:
      - microservices_net

  # 2. PHP Order Microservice Application
  order-service:
    build:
      context: ./order-service
      dockerfile: Dockerfile
    container_name: service_order_app
    restart: unless-stopped
    environment:
      DB_HOST: order-db
      DB_NAME: orders_db
      DB_USER: app_user
      DB_PASS: secret_password
      RABBITMQ_HOST: message-broker
    volumes:
      - ./order-service:/var/www/html
    depends_on:
      - order-db
      - message-broker
    networks:
      - microservices_net

  # 3. Isolated Persistence Layer for Order Microservice
  order-db:
    image: mysql:8.0
    container_name: service_order_db
    restart: always
    environment:
      MYSQL_DATABASE: orders_db
      MYSQL_USER: app_user
      MYSQL_PASSWORD: secret_password
      MYSQL_ROOT_PASSWORD: root_admin_password
    ports:
      - "33061:3306"
    volumes:
      - order_db_data:/var/lib/mysql
    networks:
      - microservices_net

  # 4. Asynchronous Message Broker (RabbitMQ)
  message-broker:
    image: rabbitmq:3-management-alpine
    container_name: shared_message_broker
    ports:
      - "5672:5672"    # AMQP protocol port
      - "15672:15672"  # Web Management UI port
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - microservices_net

networks:
  microservices_net:
    driver: bridge

volumes:
  order_db_data:
  rabbitmq_data:

Microservice Production Dockerfile (./order-service/Dockerfile)

FROM php:8.3-fpm-alpine

# Install system dependencies & PHP extensions required for microservices
RUN apk add --no-cache \
    postgresql-dev \
    libpng-dev \
    oniguruma-dev \
    libxml2-dev \
    rabbitmq-c-dev \
    git \
    unzip \
    && docker-php-ext-install pdo pdo_mysql bcmath sockets

# Install Composer globally
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

COPY . /var/www/html

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

EXPOSE 9000
CMD ["php-fpm"]

Chapter 8. Comparative Matrix Table: Monolithic Architecture vs. Microservices Architecture

Understanding the operational trade-offs between monolithic architectures and microservices ensures engineering decisions align with project constraints and organization size.

Architectural Vector

Monolithic Architecture

Microservices Architecture

Codebase Structure

Unified repository containing all domain models and views.

Multiple decoupled repositories organized per domain context.

Deployment Complexity

Low. Single executable or web archive deployed to server pool.

High. Requires automated CI/CD pipelines, Docker, & Kubernetes.

Data Persistence

Centralized database; shared tables across domain entities.

Isolated database per service; no direct cross-database reads.

Inter-Service Latency

Near Zero. In-memory function calls across application layers.

Network Latency (1-50ms+). HTTP, gRPC, or AMQP network hops.

Fault Isolation

Poor. A fatal crash in one module brings down the entire app.

High. A failure in one service is contained without crashing siblings.

Technology Flexibility

Locked to the primary language framework stack chosen initially.

Polyglot options. Services can use different languages or databases.

Team Scaling Efficiency

High collision risk as development team count increases.

Autonomous teams own, build, and deploy specific services independently.

Chapter 9. Production Launch Checklist for Microservices Developers

Before deploying a cluster of modern PHP microservices into production, run through this operational readiness checklist:

  • Enforce Strict Database Isolation: Verify that zero microservices access databases owned by sibling services directly.

  • Implement Centralized Log Aggregation: Stream stdout/stderr logs from all Docker containers to an ELK Stack (Elasticsearch, Logstash, Kibana) or Grafana Loki instance using structured JSON formats.

  • Configure Distributed Tracing: Instrument application endpoints with OpenTelemetry or Jaeger to trace HTTP and gRPC request context IDs (X-Correlation-ID) across service hops.

  • Automate Health Check Endpoints: Implement /healthz/liveness and /healthz/readiness endpoints in every PHP service for Kubernetes and API Gateway probe monitoring.

  • Establish Dead Letter Queues (DLQ): Configure RabbitMQ exchanges to redirect unprocessable or repeatedly failing messages to a dead-letter queue for developer inspection.

  • Enforce Service Mutual TLS (mTLS): Encrypt all internal service-to-service network traffic using Service Mesh proxies like Istio or Linkerd.

  • Configure Automated CI/CD Pipelines: Ensure every microservice repository runs automated PHPUnit unit tests, static analysis (PHPStan level 8+), and Docker container builds on every pull request.

Chapter 10. Frequently Asked Questions (FAQ)

❓ When should an engineering team switch from a monolith to microservices?

A team should consider switching to microservices when organizational growth causes deployment bottlenecks, when different domains require completely different scaling profiles, or when continuous integration merges become too complex. Microservices should solve organizational scale challenges; starting with microservices too early on a simple product adds unnecessary operational overhead.

❓ How do you handle distributed user authentication across microservices?

Authentication should be handled at the API Gateway layer using stateless JSON Web Tokens (JWT). The API Gateway validates the client token signature once and extracts the claims (e.g., user_id, roles). The Gateway then forwards these verified claims to internal downstream microservices via custom HTTP request headers (X-User-Id, X-User-Roles), avoiding repetitive token verification logic in every service.

❓ What is the purpose of correlation IDs in microservices logging?

A Correlation ID is a unique string (typically a UUID) generated at the API Gateway when a request first enters the system. This ID is passed along through custom HTTP or gRPC headers during all subsequent inter-service calls and asynchronous message payloads. Including the Correlation ID in log statements allows developers to query centralized log aggregators and view the complete trace of a request across all services.

❓ How do microservices handle cross-service data queries without JOINs?

Microservices avoid SQL JOINs across service boundaries by using API aggregation at the Gateway layer, maintaining localized read-model caches updated via asynchronous domain events (CQRS pattern), or making parallel internal gRPC calls to query dependent data points before returning response payloads to clients.

Komentar Pengunjung (0)

Belum ada komentar. Jadilah yang pertama memberikan komentar!