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:
- When a user places an order, the
Order service
inserts a new record into its database with the status ‘Payment Pending’. - The
Order service
then makes a request to theHistory service
via REST API or gRPC to log the order, with a message like ‘Order XYZ placed’. - Next, the
Order service
sends a request to thePayment service
to generate an invoice for the order. - The user is redirected to the payment page. Once the payment is successfully processed, the
Payment service
handles its internal business logic. - After the payment is completed, the
Payment service
must notify theOrder service
to update the order status and send another request to theHistory 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:
- Publish an
order_created
event with relevant order details. - Publish a
process_payment
command with the necessary payment information. - Implement the Outbox Pattern to ensure that these messages are published as part of the same transaction as the order creation.
What happens on the subscriber’s end?
- History Service: Listens for the
order_created
event at its own pace and updates its records accordingly. - Payment Service: Listens for the
process_payment
command and performs the payment processing logic. - 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, apayment_failed
event. - Updating Records: Upon receiving the
payment_successful
orpayment_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:
- Asynchronous Communication: Services can communicate without waiting for an immediate response, reducing latency and improving responsiveness.
- 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.
- 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.
- Scalability: Supports high scalability, as multiple consumers can process messages from the queue concurrently.
- Flexibility: Easy to implement event-driven architectures, allowing services to react to events in a decoupled manner.
Disadvantages Messaging:
- Complexity: Introduces additional complexity in managing message queues, ensuring message delivery, and handling retries and failures.
- Latency: May introduce some latency due to message queuing and asynchronous processing.
- Message Ordering: Ensuring the correct order of message processing can be challenging.
- Debugging and Monitoring: Can be harder to debug and monitor compared to synchronous communication.
- Consistency: Managing consistency across services can be more complex, especially with eventual consistency models.
Advantages of Request/Response:
- Simplicity: REST APIs are straightforward to implement and understand, using standard HTTP methods and status codes.
- Synchronous Communication: Provides immediate feedback and results, which can simplify error handling and transaction management.
- Interoperability: REST APIs are language-agnostic and can be easily consumed by various clients and services.
- Ease of Testing: Easier to test and debug due to synchronous request/response nature.
Disadvantages of Request/Response:
- Coupling: Services are more tightly coupled, as they need to know about each other’s endpoints and data formats.
- Scalability: Can be less scalable due to synchronous communication, which may lead to bottlenecks under high load.
- Error Handling: Error handling can be more complex as failures in one service can affect the entire request flow.
- Latency: Can introduce latency due to the synchronous nature of communication, where services have to wait for each other’s responses.
- 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
andRequest/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.