The way you communicate, Does It Affect Your Way of Life?

Certainly it does, communication is essential for understanding each other; nearly every living organism relies on it. While that’s not our topic today, it’s crucial to consider how communication impacts distributed systems.

So, what happens when hundreds of services need to interact with each other? How do you manage this in your daily work? Are your services becoming overly chatty and tangled, like a plate of spaghetti?

Microservices can utilize both synchronous and asynchronous approaches depending on their use cases. For synchronous communication, options like REST APIs and gRPC are commonly used. In contrast, asynchronous communication often involves messaging such as events and commands with a proper message queue or pub/sub mechanism. We’ll explore these concepts further, including choreography and orchestration, in our next article.

Let’s consider services such as Order, History, and Payment from an e-commerce platform. Taking a simple, straightforward approach:

ecommerce-diagram

Services interconnected via HTTP calls, creating a tangled network

  1. When a user places an order, the Order service inserts a new record into its database with the status ‘Payment Pending’.
  2. The Order service then makes a request to the History service via REST API or gRPC to log the order, with a message like ‘Order XYZ placed’.
  3. Next, the Order service sends a request to the Payment service to generate an invoice for the order.
  4. The user is redirected to the payment page. Once the payment is successfully processed, the Payment service handles its internal business logic.
  5. After the payment is completed, the Payment service must notify the Order service to update the order status and send another request to the History service to log the successful payment against the order.

The key point here is that microservices are designed based on their specific responsibilities. However, when implementing a single flow, such as Place a New Order, we often find ourselves making synchronous calls between services, creating a complex web of dependencies. This can resemble a tangled bowl of noodles, where it’s unclear which service is calling which and why.

Consider the following issues with previous approach:

Service Dependencies and Failures: If the History service is temporarily down, the Order service's synchronous call to it will fail, potentially causing the entire order placement process to fail. This is because the Order service is making HTTP or gRPC requests to both the History and Payment services, resulting in a request/response interaction.

Service Latency: What if all services are operational but one of them is experiencing latency? Should the Order service fail the order placement due to the delay, or should it wait for the slow History service response? This introduces a challenge in balancing timely order processing with handling slow responses from dependent services.

Prioritization of Services: From a business perspective, the placement of an order is crucial because it directly impacts revenue. In contrast, updating the History service can be less time-sensitive. Thus, the Order service should prioritize completing the order placement process and consider deferring or handling the history update at a later time if necessary.

Exploring an Alternative Approach

Instead of making synchronous HTTP REST API or gRPC calls between services, we can leverage a message queue for communication:

Publish Events and Commands:

  1. Publish an order_created event with relevant order details.
  2. Publish a process_payment command with the necessary payment information.
  3. Implement the Outbox Pattern to ensure that these messages are published as part of the same transaction as the order creation.
ecommerce-diagram

Services responding to events at their own pace asynchronously

What happens on the subscriber’s end?

  1. History Service: Listens for the order_created event at its own pace and updates its records accordingly.
  2. Payment Service: Listens for the process_payment command and performs the payment processing logic.
  3. Post-Payment Processing: Once the user is redirected to the payment page and successfully completes the payment, the Payment Service publishes a payment_successful event or, if the payment fails, a payment_failed event.
  4. Updating Records: Upon receiving the payment_successful or payment_failed event, both the Order Service and History Service can update their records to reflect the final status of the order.

Trade-offs between Messaging & Request/Response approach

Advantages of Messsging:

  1. Asynchronous Communication: Services can communicate without waiting for an immediate response, reducing latency and improving responsiveness.
  2. Decoupling: Services are loosely coupled, as they don’t need to know about each other’s implementation details. This allows for more flexible scaling and maintenance.
  3. Resilience and Fault Tolerance: If a service is down or experiencing slow performance, messages can be queued and processed later, thereby mitigating the impact of service failures. Services can process messages at their own pace and according to their processing capacity, allowing for better handling of varying loads and system resilience.
  4. Scalability: Supports high scalability, as multiple consumers can process messages from the queue concurrently.
  5. Flexibility: Easy to implement event-driven architectures, allowing services to react to events in a decoupled manner.

Disadvantages Messaging:

  1. Complexity: Introduces additional complexity in managing message queues, ensuring message delivery, and handling retries and failures.
  2. Latency: May introduce some latency due to message queuing and asynchronous processing.
  3. Message Ordering: Ensuring the correct order of message processing can be challenging.
  4. Debugging and Monitoring: Can be harder to debug and monitor compared to synchronous communication.
  5. Consistency: Managing consistency across services can be more complex, especially with eventual consistency models.

Advantages of Request/Response:

  1. Simplicity: REST APIs are straightforward to implement and understand, using standard HTTP methods and status codes.
  2. Synchronous Communication: Provides immediate feedback and results, which can simplify error handling and transaction management.
  3. Interoperability: REST APIs are language-agnostic and can be easily consumed by various clients and services.
  4. Ease of Testing: Easier to test and debug due to synchronous request/response nature.

Disadvantages of Request/Response:

  1. Coupling: Services are more tightly coupled, as they need to know about each other’s endpoints and data formats.
  2. Scalability: Can be less scalable due to synchronous communication, which may lead to bottlenecks under high load.
  3. Error Handling: Error handling can be more complex as failures in one service can affect the entire request flow.
  4. Latency: Can introduce latency due to the synchronous nature of communication, where services have to wait for each other’s responses.
  5. Reliability: If a service is down, the calling service may experience downtime or failures, affecting overall system reliability.

Each approach has its own strengths and is suitable for different scenarios. The choice between Messaging and Request/Response often depends on the specific requirements of the system, such as the need for asynchronous processing, scalability, or real-time communication. In many real-world systems, a hybrid approach works best.