Skip to Content
Course content

242: Service Communication Patterns in PHP Microservices

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a piece of code that looks perfectly fine during a code review, but will absolutely tank your production environment the moment you hit a certain scale. This is a classic "distributed monolith" mistake.

// In CheckoutService.php
public function completeOrder(Order $order) 
{
    // 1. Mark order as paid
    $this->orderRepo->markAsPaid($order);

    // 2. Tell Inventory Service to reserve items (Synchronous HTTP)
    $inventoryResponse = $this->httpClient->post('http://inventory-service/reserve', [
        'json' => ['order_id' => $order->getId()]
    ]);

    // 3. Tell Shipping Service to create a label (Synchronous HTTP)
    $shippingResponse = $this->httpClient->post('http://shipping-service/label', [
        'json' => ['order_id' => $order->getId()]
    ]);

    // 4. Send a confirmation email via Notification Service (Synchronous HTTP)
    $this->httpClient->post('http://notification-service/send', [
        'json' => ['email' => $order->getUserEmail(), 'template' => 'order_confirmed']
    ]);

    return new OrderResponse('Order processed successfully!');
}

The Cascading Failure Trap

At first glance, this is logical. You're coordinating three different services. But here is the reality: you've just created a synchronous chain of dependencies. If the notification-service has a network hiccup or takes 5 seconds to respond, your CheckoutService is blocked. The PHP-FPM worker is sitting there, idling, holding a connection open, and waiting.

If the shipping service slows down and the notification service hangs, your entire checkout process grinds to a halt. This is called a cascading failure. Your users aren't seeing an "Order Confirmed" page; they're seeing a 504 Gateway Timeout because the request timed out at the Nginx level while PHP was still waiting on a distant API call. I've spent way too many weekends debugging these "ghost" outages that only happen during high-traffic sales.

Moving to Asynchronous Event-Driven Communication

The fix isn't to just "increase the timeout." The fix is to ask yourself: "Does the user actually need to wait for this to finish before they get a success message?"

The user needs to know the order was placed. They do not need to wait for the shipping label to be generated or the email to be sent. We should only keep the absolutely critical operations synchronous (like inventory reservation) and move everything else to an asynchronous pattern using a message broker (like RabbitMQ, Redis, or Amazon SQS).

// In CheckoutService.php (The Refactored Version)
public function completeOrder(Order $order) 
{
    $this->orderRepo->markAsPaid($order);

    // CRITICAL: We keep this sync because we can't sell what we don't have.
    // If this fails, we want to tell the user immediately.
    $this->inventoryService->reserveItems($order);

    // NON-CRITICAL: Fire and forget. 
    // We push a message to a queue and let a background worker handle it.
    $this->messageBus->dispatch(new OrderPlacedEvent($order->getId()));

    return new OrderResponse('Order processed successfully!');
}

Now, the ShippingService and NotificationService are no longer called directly by the Checkout process. Instead, they are "subscribers" to the OrderPlacedEvent. They pick up the message from the queue and process it at their own pace. If the email service goes down for ten minutes, the messages just sit in the queue. Once it comes back online, it processes the backlog. The user never knows there was a problem; their checkout was instant.

Choosing Between Request-Response and Pub/Sub

You can't move everything to a queue, though. You need to choose your pattern based on the requirement:

  • Request-Response (Synchronous): Use this when you need an immediate answer to proceed. Example: Validating a credit card or checking stock levels. Use a circuit breaker pattern here so that if the remote service is down, you fail fast rather than hanging.
  • Publish/Subscribe (Asynchronous): Use this for side effects. Example: Sending emails, updating search indexes, or notifying a warehouse. This decouples your services and makes your system significantly more resilient.

A good rule of thumb I use: if the operation is a "side effect" of the main action, it belongs in a queue.




📋 Practical Task

Implement a Decoupled Order Notification System

You have a legacy OrderController that currently calls a SmsNotificationService via a synchronous HTTP call, causing the checkout process to lag whenever the SMS gateway is slow. Your task is to refactor this into an asynchronous pattern.

Requirements:

  • Create an OrderPlacedEvent class that holds the orderId.
  • Modify the OrderController to remove the direct $this->smsService->send(...) call and instead dispatch the OrderPlacedEvent to a mock MessageBus.
  • Implement an OrderNotificationHandler that "listens" for the OrderPlacedEvent and handles the actual SMS sending logic.
  • Ensure that the OrderController returns a response to the user immediately after dispatching the event, without waiting for the SMS to be sent.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.