Microservices architecture distributes functionality across independent services. How these services communicate determines system reliability, performance, and complexity. This guide covers communication patterns from synchronous REST calls to asynchronous event-driven architectures.
Communication Styles
Synchronous vs Asynchronous
Synchronous: Caller waits for response
Order Service → [HTTP Request] → Payment Service
← [HTTP Response] ←
Asynchronous: Caller doesn't wait
Order Service → [Message Queue] → Payment Service
(continues immediately)
One-to-One vs One-to-Many
One-to-One: Single sender, single receiver One-to-Many: Single sender, multiple receivers (pub/sub)
Synchronous Patterns
REST APIs
The most common pattern. Services expose HTTP endpoints:
// Order Service calling Payment Service
class PaymentClient
{
public function charge(Order $order): PaymentResult
{
$response = Http::timeout(5)
->retry(3, 100)
->post('http://payment-service/api/charges', [
'order_id' => $order->id,
'amount' => $order->total,
'currency' => $order->currency,
]);
if ($response->failed()) {
throw new PaymentFailedException($response->body());
}
return PaymentResult::fromResponse($response->json());
}
}
Pros:
- Simple, well-understood
- Stateless
- Easy debugging
Cons:
- Tight coupling
- Cascading failures
- Latency accumulation
gRPC
Binary protocol with strong typing:
// payment.proto
service PaymentService {
rpc Charge(ChargeRequest) returns (ChargeResponse);
}
message ChargeRequest {
string order_id = 1;
int64 amount_cents = 2;
string currency = 3;
}
message ChargeResponse {
string transaction_id = 1;
string status = 2;
}
// PHP gRPC client
$client = new PaymentServiceClient('payment-service:50051', [
'credentials' => ChannelCredentials::createInsecure(),
]);
$request = new ChargeRequest();
$request->setOrderId($order->id);
$request->setAmountCents($order->total_cents);
[$response, $status] = $client->Charge($request)->wait();
Pros:
- High performance (binary, HTTP/2)
- Strong typing with code generation
- Bi-directional streaming
Cons:
- More complex setup
- Harder to debug
- Limited browser support
Service Discovery
Services need to find each other:
// Using Consul for service discovery
class ServiceRegistry
{
public function getServiceUrl(string $service): string
{
$response = Http::get("http://consul:8500/v1/catalog/service/{$service}");
$instances = $response->json();
// Simple round-robin
$instance = $instances[array_rand($instances)];
return "http://{$instance['ServiceAddress']}:{$instance['ServicePort']}";
}
}
// Usage
$paymentUrl = $registry->getServiceUrl('payment-service');
Circuit Breaker
Prevent cascading failures:
class CircuitBreaker
{
private int $failures = 0;
private int $threshold = 5;
private ?Carbon $openedAt = null;
private int $timeout = 30;
public function call(callable $operation): mixed
{
if ($this->isOpen()) {
throw new CircuitOpenException();
}
try {
$result = $operation();
$this->recordSuccess();
return $result;
} catch (Exception $e) {
$this->recordFailure();
throw $e;
}
}
private function isOpen(): bool
{
if ($this->failures < $this->threshold) {
return false;
}
if ($this->openedAt->addSeconds($this->timeout)->isPast()) {
// Half-open: allow one attempt
return false;
}
return true;
}
}
Asynchronous Patterns
Message Queues
Decouple services with queued messages:
// Order Service publishes event
class OrderService
{
public function create(array $data): Order
{
$order = Order::create($data);
// Publish to queue instead of calling payment service
OrderCreated::dispatch($order);
return $order;
}
}
// Payment Service consumes event
class ProcessOrderPayment implements ShouldQueue
{
public function handle(OrderCreated $event): void
{
$order = $event->order;
$this->paymentGateway->charge($order);
// Publish result
PaymentProcessed::dispatch($order, $result);
}
}
Queue Technologies:
- RabbitMQ: Feature-rich, routing, persistence
- Amazon SQS: Managed, scalable, simple
- Redis: Fast, simple, good for Laravel
- Apache Kafka: High-throughput, event streaming
Event-Driven Architecture
Services react to events rather than direct calls:
// Events published to message broker
class OrderService
{
public function create(array $data): Order
{
$order = Order::create($data);
$this->eventBus->publish('orders', new OrderCreated([
'order_id' => $order->id,
'user_id' => $order->user_id,
'total' => $order->total,
'items' => $order->items->toArray(),
]));
return $order;
}
}
// Multiple services subscribe to same event
// Payment Service
$eventBus->subscribe('orders', 'OrderCreated', function ($event) {
$this->chargeCustomer($event->order_id, $event->total);
});
// Inventory Service
$eventBus->subscribe('orders', 'OrderCreated', function ($event) {
$this->reserveInventory($event->items);
});
// Notification Service
$eventBus->subscribe('orders', 'OrderCreated', function ($event) {
$this->sendConfirmationEmail($event->user_id, $event->order_id);
});
Saga Pattern
Coordinate distributed transactions:
class OrderSaga
{
private array $steps = [];
private array $compensations = [];
public function execute(Order $order): void
{
try {
// Step 1: Reserve inventory
$this->reserveInventory($order);
$this->compensations[] = fn() => $this->releaseInventory($order);
// Step 2: Process payment
$this->processPayment($order);
$this->compensations[] = fn() => $this->refundPayment($order);
// Step 3: Confirm order
$this->confirmOrder($order);
} catch (Exception $e) {
// Compensate in reverse order
$this->compensate();
throw $e;
}
}
private function compensate(): void
{
foreach (array_reverse($this->compensations) as $compensation) {
try {
$compensation();
} catch (Exception $e) {
Log::error('Compensation failed', ['error' => $e->getMessage()]);
}
}
}
}
Event Sourcing
Store events instead of current state:
class OrderAggregate
{
private array $events = [];
private string $status = 'pending';
public function create(array $items): void
{
$this->recordEvent(new OrderCreated($items));
}
public function confirm(): void
{
if ($this->status !== 'pending') {
throw new InvalidOrderStateException();
}
$this->recordEvent(new OrderConfirmed());
}
private function recordEvent(DomainEvent $event): void
{
$this->events[] = $event;
$this->apply($event);
}
private function apply(DomainEvent $event): void
{
match(get_class($event)) {
OrderCreated::class => $this->status = 'pending',
OrderConfirmed::class => $this->status = 'confirmed',
OrderCancelled::class => $this->status = 'cancelled',
};
}
}
API Gateway
Single entry point for external clients:
Client → API Gateway → Service A
→ Service B
→ Service C
Responsibilities:
- Request routing
- Authentication
- Rate limiting
- Response aggregation
- Protocol translation
// Laravel as API Gateway
class OrderController extends Controller
{
public function show(int $id)
{
// Aggregate data from multiple services
$order = Http::get("http://order-service/orders/{$id}")->json();
$user = Http::get("http://user-service/users/{$order['user_id']}")->json();
$payments = Http::get("http://payment-service/orders/{$id}/payments")->json();
return response()->json([
'order' => $order,
'user' => [
'name' => $user['name'],
'email' => $user['email'],
],
'payments' => $payments,
]);
}
}
Service Mesh
Infrastructure layer for service communication:
# Istio VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: payment-service
spec:
hosts:
- payment-service
http:
- timeout: 5s
retries:
attempts: 3
perTryTimeout: 2s
route:
- destination:
host: payment-service
port:
number: 80
Features:
- mTLS encryption
- Traffic management
- Observability
- Retries and timeouts
- Circuit breaking
Choosing Patterns
| Scenario | Recommended Pattern |
|---|---|
| Simple CRUD operations | REST API |
| High-performance internal | gRPC |
| Loose coupling needed | Message Queue |
| Multiple consumers | Event-Driven / Pub-Sub |
| Distributed transactions | Saga Pattern |
| External API clients | API Gateway |
| Complex networking needs | Service Mesh |
Best Practices
- Design for failure: Assume any call can fail
- Set timeouts: Don't wait forever
- Implement retries: With exponential backoff
- Use circuit breakers: Prevent cascading failures
- Make operations idempotent: Safe to retry
- Version your APIs: Support gradual migration
- Monitor everything: Tracing, metrics, logs
- Document contracts: Clear API specifications
Conclusion
Microservices communication patterns range from simple HTTP calls to sophisticated event-driven architectures. Start with synchronous REST for simplicity, add message queues for decoupling, and implement circuit breakers for resilience. The right pattern depends on your consistency requirements, latency tolerance, and operational complexity appetite.