Microservices Communication Patterns

Reverend Philip Dec 15, 2025 6 min read

Design effective communication between microservices. Compare synchronous and asynchronous patterns, API gateways, and service meshes.

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

  1. Design for failure: Assume any call can fail
  2. Set timeouts: Don't wait forever
  3. Implement retries: With exponential backoff
  4. Use circuit breakers: Prevent cascading failures
  5. Make operations idempotent: Safe to retry
  6. Version your APIs: Support gradual migration
  7. Monitor everything: Tracing, metrics, logs
  8. 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.

Share this article

Related Articles

Distributed Locking Patterns

Coordinate access to shared resources across services. Implement distributed locks with Redis, ZooKeeper, and databases.

Jan 16, 2026

Need help with your project?

Let's discuss how we can help you build reliable software.