Advanced Laravel

Microservices Communication

18 min Lesson 18 of 40

Microservices Communication

As applications grow, splitting them into independent microservices improves scalability, maintainability, and deployment flexibility. Laravel provides excellent tools for building microservices architectures, including HTTP clients for synchronous communication, queue-based messaging for asynchronous patterns, and event-driven architectures for loosely coupled systems. This lesson explores patterns and implementations for inter-service communication.

HTTP Client for Service-to-Service Communication

Laravel's HTTP client, built on Guzzle, provides a fluent interface for making HTTP requests between microservices with features like retries, timeouts, and middleware.

<?php

// config/services.php - Define service endpoints
return [
    'user_service' => [
        'url' => env('USER_SERVICE_URL', 'http://user-service:8001'),
        'key' => env('USER_SERVICE_KEY'),
        'timeout' => 10,
    ],
    'payment_service' => [
        'url' => env('PAYMENT_SERVICE_URL', 'http://payment-service:8002'),
        'key' => env('PAYMENT_SERVICE_KEY'),
        'timeout' => 30,
    ],
    'notification_service' => [
        'url' => env('NOTIFICATION_SERVICE_URL', 'http://notification-service:8003'),
        'key' => env('NOTIFICATION_SERVICE_KEY'),
        'timeout' => 5,
    ],
];

// app/Services/UserServiceClient.php
namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class UserServiceClient
{
    protected string $baseUrl;
    protected string $apiKey;
    protected int $timeout;

    public function __construct()
    {
        $this->baseUrl = config('services.user_service.url');
        $this->apiKey = config('services.user_service.key');
        $this->timeout = config('services.user_service.timeout');
    }

    protected function client(): PendingRequest
    {
        return Http::baseUrl($this->baseUrl)
            ->withToken($this->apiKey)
            ->timeout($this->timeout)
            ->retry(3, 100) // Retry 3 times with 100ms delay
            ->withHeaders([
                'X-Service-Name' => config('app.name'),
                'X-Request-ID' => request()->header('X-Request-ID', uniqid()),
            ]);
    }

    public function getUser(int $userId): ?array
    {
        try {
            $response = $this->client()->get("/users/{$userId}");

            if ($response->successful()) {
                return $response->json('data');
            }

            Log::warning("Failed to fetch user {$userId}", [
                'status' => $response->status(),
                'body' => $response->body(),
            ]);

            return null;
        } catch (\Exception $e) {
            Log::error("User service communication failed: {$e->getMessage()}");
            return null;
        }
    }

    public function createUser(array $data): ?array
    {
        $response = $this->client()
            ->post('/users', $data);

        return $response->successful() ? $response->json('data') : null;
    }

    public function updateUser(int $userId, array $data): bool
    {
        $response = $this->client()
            ->put("/users/{$userId}", $data);

        return $response->successful();
    }

    public function getUsersByIds(array $userIds): array
    {
        $response = $this->client()
            ->post('/users/batch', ['ids' => $userIds]);

        return $response->successful() ? $response->json('data', []) : [];
    }
}

// Usage in Controller
use App\Services\UserServiceClient;

class OrderController extends Controller
{
    public function __construct(
        protected UserServiceClient $userService
    ) {}

    public function store(Request $request)
    {
        // Validate order
        $validated = $request->validate([
            'user_id' => 'required|integer',
            'items' => 'required|array',
        ]);

        // Fetch user from user service
        $user = $this->userService->getUser($validated['user_id']);

        if (!$user) {
            return response()->json([
                'message' => 'User not found'
            ], 404);
        }

        // Create order
        $order = Order::create([
            'user_id' => $validated['user_id'],
            'user_email' => $user['email'], // Cache user data
            'items' => $validated['items'],
            'total' => $this->calculateTotal($validated['items']),
        ]);

        return response()->json($order, 201);
    }
}
Best Practice: Always implement circuit breaker patterns to prevent cascading failures. If a service is down, fail fast instead of waiting for timeouts. Consider using packages like "resilience4php/resilience4php" for sophisticated circuit breaker implementations.

Async HTTP Requests with Concurrent Pool

When you need to call multiple services simultaneously, Laravel's HTTP pool allows parallel requests, significantly reducing total latency.

<?php

use Illuminate\Support\Facades\Http;

// Sequential requests (slow - 300ms total)
$user = Http::get('http://user-service/users/1'); // 100ms
$orders = Http::get('http://order-service/orders?user_id=1'); // 100ms
$payments = Http::get('http://payment-service/payments?user_id=1'); // 100ms

// Concurrent requests (fast - 100ms total)
$responses = Http::pool(fn ($pool) => [
    $pool->as('user')->get('http://user-service/users/1'),
    $pool->as('orders')->get('http://order-service/orders?user_id=1'),
    $pool->as('payments')->get('http://payment-service/payments?user_id=1'),
]);

$user = $responses['user']->json();
$orders = $responses['orders']->json();
$payments = $responses['payments']->json();

// Advanced pool with error handling
class DashboardService
{
    public function getUserDashboard(int $userId): array
    {
        $responses = Http::pool(function ($pool) use ($userId) {
            return [
                $pool->as('profile')
                    ->timeout(5)
                    ->get("http://user-service/users/{$userId}"),

                $pool->as('orders')
                    ->timeout(10)
                    ->get("http://order-service/users/{$userId}/orders"),

                $pool->as('notifications')
                    ->timeout(3)
                    ->get("http://notification-service/users/{$userId}/notifications"),

                $pool->as('stats')
                    ->timeout(8)
                    ->get("http://analytics-service/users/{$userId}/stats"),
            ];
        });

        return [
            'profile' => $responses['profile']->successful()
                ? $responses['profile']->json()
                : ['error' => 'Profile unavailable'],

            'orders' => $responses['orders']->successful()
                ? $responses['orders']->json()
                : [],

            'notifications' => $responses['notifications']->successful()
                ? $responses['notifications']->json()
                : [],

            'stats' => $responses['stats']->successful()
                ? $responses['stats']->json()
                : $this->getDefaultStats(),
        ];
    }

    protected function getDefaultStats(): array
    {
        return [
            'orders_count' => 0,
            'total_spent' => 0,
        ];
    }
}

// Circuit breaker pattern
class ServiceClient
{
    protected bool $isOpen = false;
    protected int $failureCount = 0;
    protected int $threshold = 5;
    protected int $timeout = 60; // seconds

    public function call(string $url): mixed
    {
        if ($this->isOpen) {
            if ($this->shouldAttemptReset()) {
                $this->halfOpen();
            } else {
                throw new \Exception('Circuit breaker is open');
            }
        }

        try {
            $response = Http::timeout(5)->get($url);

            if ($response->successful()) {
                $this->onSuccess();
                return $response->json();
            }

            $this->onFailure();
            return null;
        } catch (\Exception $e) {
            $this->onFailure();
            throw $e;
        }
    }

    protected function onSuccess(): void
    {
        $this->failureCount = 0;
        $this->isOpen = false;
    }

    protected function onFailure(): void
    {
        $this->failureCount++;

        if ($this->failureCount >= $this->threshold) {
            $this->open();
        }
    }

    protected function open(): void
    {
        $this->isOpen = true;
        cache()->put('circuit_breaker_opened_at', now(), $this->timeout);
    }

    protected function shouldAttemptReset(): bool
    {
        $openedAt = cache()->get('circuit_breaker_opened_at');
        return $openedAt && now()->diffInSeconds($openedAt) >= $this->timeout;
    }

    protected function halfOpen(): void
    {
        $this->isOpen = false;
        $this->failureCount = $this->threshold - 1;
    }
}
Performance Tip: Use HTTP pools for aggregating data from multiple services. This can reduce total response time from sum of all requests to the time of the slowest request.

Message Queues for Asynchronous Communication

For operations that don't require immediate responses, message queues provide reliable asynchronous communication between services, improving resilience and scalability.

<?php

// Order Service - Publishes events
// app/Jobs/PublishOrderCreatedEvent.php
namespace App\Jobs;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;

class PublishOrderCreatedEvent implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}

    public function handle(): void
    {
        $payload = [
            'event' => 'order.created',
            'data' => [
                'order_id' => $this->order->id,
                'user_id' => $this->order->user_id,
                'total' => $this->order->total,
                'items' => $this->order->items,
                'created_at' => $this->order->created_at->toIso8601String(),
            ],
            'timestamp' => now()->toIso8601String(),
        ];

        // Publish to message broker (RabbitMQ, Redis, etc.)
        $this->publishToQueue('order-events', $payload);

        // Or send to multiple services
        $this->notifyInventoryService($payload);
        $this->notifyPaymentService($payload);
        $this->notifyNotificationService($payload);
    }

    protected function publishToQueue(string $queue, array $payload): void
    {
        // Using Redis as message broker
        \Illuminate\Support\Facades\Redis::rpush($queue, json_encode($payload));
    }

    protected function notifyInventoryService(array $payload): void
    {
        Http::baseUrl(config('services.inventory_service.url'))
            ->timeout(5)
            ->post('/events/order-created', $payload);
    }

    protected function notifyPaymentService(array $payload): void
    {
        Http::baseUrl(config('services.payment_service.url'))
            ->timeout(5)
            ->post('/events/order-created', $payload);
    }

    protected function notifyNotificationService(array $payload): void
    {
        Http::baseUrl(config('services.notification_service.url'))
            ->timeout(5)
            ->post('/events/order-created', $payload);
    }
}

// Notification Service - Consumes events
// app/Console/Commands/ConsumeOrderEvents.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;

class ConsumeOrderEvents extends Command
{
    protected $signature = 'queue:consume-order-events';
    protected $description = 'Consume order events from message queue';

    public function handle(): void
    {
        $this->info('Listening for order events...');

        while (true) {
            $message = Redis::blpop('order-events', 0); // Blocking pop

            if ($message) {
                $payload = json_decode($message[1], true);
                $this->processEvent($payload);
            }
        }
    }

    protected function processEvent(array $payload): void
    {
        $event = $payload['event'];
        $data = $payload['data'];

        match ($event) {
            'order.created' => $this->handleOrderCreated($data),
            'order.updated' => $this->handleOrderUpdated($data),
            'order.cancelled' => $this->handleOrderCancelled($data),
            default => $this->warn("Unknown event: {$event}"),
        };
    }

    protected function handleOrderCreated(array $data): void
    {
        $this->info("Processing order.created: {$data['order_id']}");

        // Send notification to user
        $user = $this->fetchUser($data['user_id']);

        if ($user) {
            $this->sendNotification($user['email'], [
                'subject' => 'Order Confirmation',
                'message' => "Your order #{$data['order_id']} has been placed successfully.",
            ]);
        }
    }

    protected function fetchUser(int $userId): ?array
    {
        $response = Http::get(config('services.user_service.url') . "/users/{$userId}");
        return $response->successful() ? $response->json('data') : null;
    }

    protected function sendNotification(string $email, array $data): void
    {
        // Send email notification
        \Illuminate\Support\Facades\Mail::raw($data['message'], function ($message) use ($email, $data) {
            $message->to($email)->subject($data['subject']);
        });
    }
}

// Register consumer as a daemon
// supervisor configuration: /etc/supervisor/conf.d/order-event-consumer.conf
[program:order-event-consumer]
command=php /path/to/artisan queue:consume-order-events
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/order-event-consumer.log

Event-Driven Architecture with Laravel Events

Laravel's event system can be extended to create an event-driven microservices architecture where services react to events rather than direct calls, promoting loose coupling.

<?php

// Shared event contracts across services
// packages/shared/src/Events/OrderEvent.php
namespace Shared\Events;

interface OrderEvent
{
    public function getOrderId(): int;
    public function getUserId(): int;
    public function getPayload(): array;
}

// Order Service - Publishes domain events
// app/Events/OrderCreated.php
namespace App\Events;

use Shared\Events\OrderEvent;

class OrderCreated implements OrderEvent
{
    public function __construct(
        public int $orderId,
        public int $userId,
        public float $total,
        public array $items
    ) {}

    public function getOrderId(): int
    {
        return $this->orderId;
    }

    public function getUserId(): int
    {
        return $this->userId;
    }

    public function getPayload(): array
    {
        return [
            'order_id' => $this->orderId,
            'user_id' => $this->userId,
            'total' => $this->total,
            'items' => $this->items,
            'created_at' => now()->toIso8601String(),
        ];
    }
}

// app/Listeners/PublishOrderCreatedToEventBus.php
namespace App\Listeners;

use App\Events\OrderCreated;
use App\Services\EventBusService;

class PublishOrderCreatedToEventBus
{
    public function __construct(
        protected EventBusService $eventBus
    ) {}

    public function handle(OrderCreated $event): void
    {
        $this->eventBus->publish('order.created', $event->getPayload());
    }
}

// app/Services/EventBusService.php
namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;

class EventBusService
{
    public function publish(string $eventName, array $payload): void
    {
        $message = [
            'event' => $eventName,
            'data' => $payload,
            'service' => config('app.name'),
            'timestamp' => now()->toIso8601String(),
            'correlation_id' => request()->header('X-Correlation-ID', uniqid()),
        ];

        // Publish to Redis Streams (better than lists for event sourcing)
        Redis::xadd('event-bus', '*', [
            'message' => json_encode($message)
        ]);

        // Also notify webhook subscribers
        $this->notifyWebhookSubscribers($eventName, $message);
    }

    protected function notifyWebhookSubscribers(string $eventName, array $message): void
    {
        $subscribers = $this->getWebhookSubscribers($eventName);

        foreach ($subscribers as $subscriber) {
            Http::timeout(5)
                ->withHeaders([
                    'X-Event-Name' => $eventName,
                    'X-Event-ID' => $message['correlation_id'],
                ])
                ->post($subscriber['url'], $message);
        }
    }

    protected function getWebhookSubscribers(string $eventName): array
    {
        // Fetch from database or config
        return \App\Models\WebhookSubscription::where('event', $eventName)
            ->where('is_active', true)
            ->get()
            ->toArray();
    }
}

// Inventory Service - Subscribes to events
// app/Console/Commands/ConsumeEventBus.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;

class ConsumeEventBus extends Command
{
    protected $signature = 'events:consume {--group=inventory-service}';

    public function handle(): void
    {
        $group = $this->option('group');
        $consumer = gethostname();

        // Create consumer group if not exists
        try {
            Redis::xgroup('CREATE', 'event-bus', $group, '0');
        } catch (\Exception $e) {
            // Group already exists
        }

        $this->info("Consuming events as {$group}:{$consumer}");

        while (true) {
            $events = Redis::xreadgroup(
                $group,
                $consumer,
                ['event-bus' => '>'],
                1,
                2000 // 2 second block
            );

            if (empty($events)) {
                continue;
            }

            foreach ($events['event-bus'] as $id => $event) {
                $message = json_decode($event['message'], true);
                $this->processEvent($message);

                // Acknowledge processing
                Redis::xack('event-bus', $group, [$id]);
            }
        }
    }

    protected function processEvent(array $message): void
    {
        $eventName = $message['event'];
        $data = $message['data'];

        $this->info("Processing {$eventName}");

        match ($eventName) {
            'order.created' => $this->reserveInventory($data),
            'order.cancelled' => $this->releaseInventory($data),
            default => null,
        };
    }

    protected function reserveInventory(array $data): void
    {
        foreach ($data['items'] as $item) {
            \App\Models\Inventory::where('product_id', $item['product_id'])
                ->decrement('quantity', $item['quantity']);
        }

        $this->info("Reserved inventory for order {$data['order_id']}");
    }

    protected function releaseInventory(array $data): void
    {
        foreach ($data['items'] as $item) {
            \App\Models\Inventory::where('product_id', $item['product_id'])
                ->increment('quantity', $item['quantity']);
        }

        $this->info("Released inventory for order {$data['order_id']}");
    }
}
Distributed Transactions Warning: Microservices architecture makes ACID transactions across services impossible. Implement the Saga pattern or eventual consistency patterns to handle distributed transactions. Always design for idempotency to handle duplicate messages.

API Gateway Pattern

An API Gateway provides a single entry point for clients, handling routing, authentication, rate limiting, and response aggregation from multiple microservices.

<?php

// API Gateway Service
// app/Http/Controllers/GatewayController.php
namespace App\Http\Controllers;

use App\Services\UserServiceClient;
use App\Services\OrderServiceClient;
use App\Services\PaymentServiceClient;
use Illuminate\Http\Request;

class GatewayController extends Controller
{
    public function __construct(
        protected UserServiceClient $userService,
        protected OrderServiceClient $orderService,
        protected PaymentServiceClient $paymentService
    ) {}

    // Aggregate data from multiple services
    public function getUserProfile(Request $request, int $userId)
    {
        // Parallel requests
        $responses = \Illuminate\Support\Facades\Http::pool(fn ($pool) => [
            $pool->as('user')->get(config('services.user_service.url') . "/users/{$userId}"),
            $pool->as('orders')->get(config('services.order_service.url') . "/users/{$userId}/orders"),
            $pool->as('payments')->get(config('services.payment_service.url') . "/users/{$userId}/payments"),
        ]);

        return response()->json([
            'user' => $responses['user']->json('data'),
            'orders' => $responses['orders']->json('data', []),
            'payments' => $responses['payments']->json('data', []),
            'cached_at' => now()->toIso8601String(),
        ]);
    }

    // Route to appropriate service
    public function proxy(Request $request, string $service, string $path)
    {
        $serviceUrl = config("services.{$service}.url");

        if (!$serviceUrl) {
            return response()->json(['error' => 'Service not found'], 404);
        }

        $response = \Illuminate\Support\Facades\Http::withHeaders([
            'X-Forwarded-For' => $request->ip(),
            'X-Original-URI' => $request->getRequestUri(),
        ])
        ->send($request->method(), "{$serviceUrl}/{$path}", [
            'query' => $request->query(),
            'json' => $request->json()->all(),
        ]);

        return response($response->body(), $response->status())
            ->withHeaders($response->headers());
    }
}

// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
    // Aggregated endpoints
    Route::get('/users/{userId}/profile', [GatewayController::class, 'getUserProfile']);

    // Proxy to services
    Route::any('/proxy/{service}/{path}', [GatewayController::class, 'proxy'])
        ->where('path', '.*');
});

Exercise 1: Build Service Clients

Create HTTP clients for communicating with multiple services:

  1. Implement UserServiceClient with methods: getUser(), createUser(), updateUser()
  2. Implement ProductServiceClient with methods: getProduct(), getProductsByIds()
  3. Add retry logic, timeout configuration, and error handling to both clients
  4. Implement circuit breaker pattern to prevent cascade failures
  5. Test clients with mock services and verify retry behavior

Exercise 2: Event-Driven Order Processing

Build an event-driven order processing system:

  1. Create OrderCreated event in Order Service
  2. Publish event to Redis Streams when order is created
  3. Create consumer in Inventory Service that reserves stock
  4. Create consumer in Notification Service that sends confirmation email
  5. Implement idempotency to prevent duplicate processing
  6. Test with multiple order creations and verify all services react correctly

Exercise 3: API Gateway Implementation

Build a simple API Gateway:

  1. Create gateway that routes /api/users/* to User Service
  2. Route /api/orders/* to Order Service
  3. Implement authentication middleware that validates JWT tokens
  4. Add rate limiting per user (100 requests per minute)
  5. Create aggregated endpoint /api/dashboard that fetches data from 3+ services in parallel
  6. Test gateway with multiple services and verify routing and aggregation work correctly

Summary

In this lesson, you've learned how to build microservices communication patterns in Laravel. You explored synchronous HTTP communication with service clients and parallel requests, asynchronous message queues for reliable inter-service communication, event-driven architectures using Laravel's event system and Redis Streams, and the API Gateway pattern for unifying service access. These patterns enable you to build scalable, resilient, distributed systems that can grow independently and handle failures gracefully.