Microservices Communication
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);
}
}
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;
}
}
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']}");
}
}
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:
- Implement UserServiceClient with methods: getUser(), createUser(), updateUser()
- Implement ProductServiceClient with methods: getProduct(), getProductsByIds()
- Add retry logic, timeout configuration, and error handling to both clients
- Implement circuit breaker pattern to prevent cascade failures
- Test clients with mock services and verify retry behavior
Exercise 2: Event-Driven Order Processing
Build an event-driven order processing system:
- Create OrderCreated event in Order Service
- Publish event to Redis Streams when order is created
- Create consumer in Inventory Service that reserves stock
- Create consumer in Notification Service that sends confirmation email
- Implement idempotency to prevent duplicate processing
- Test with multiple order creations and verify all services react correctly
Exercise 3: API Gateway Implementation
Build a simple API Gateway:
- Create gateway that routes /api/users/* to User Service
- Route /api/orders/* to Order Service
- Implement authentication middleware that validates JWT tokens
- Add rate limiting per user (100 requests per minute)
- Create aggregated endpoint /api/dashboard that fetches data from 3+ services in parallel
- 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.