We continue our series of posts on microservices patterns. In previous articles, we explored what a microservices architecture is and examined architectural patterns focused on the organization and structure of microservices.
In this new article, we will discuss patterns that address communication and coordination challenges between microservices.
In the context of microservices architectures, communication and coordination patterns play a crucial role in tackling the inherent challenges of efficiently managing distributed services.
These patterns provide recurring solutions to specific problems, ensuring consistency, reliability, and operational efficiency in complex systems composed of numerous microservices.
We will explore service orchestration, service choreography, API Gateway, service discovery, Event Sourcing, CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend), SAGA, and more.
Some of these patterns are already built into PaaS solutions, eliminating the need for explicit implementation.
We’ll start with two patterns that, despite being very different, are best understood when explained together: communication and coordination between microservices.
Service Orchestration
Service orchestration is a microservices architecture pattern where a central component, known as the orchestrator, coordinates the execution of multiple services to achieve a specific goal. Instead of having each microservice manage end-to-end coordination, this responsibility is delegated to the orchestrator.
Key characteristics
- Orchestrator
A central component that manages the sequence and workflow of microservices to fulfill a specific use case. - Coordination
The orchestrator ensures the correct execution order of individual services, applying the necessary business logic. - Synchronous Communication
Microservices communication is often synchronous, meaning a service waits for a response before proceeding. - Business Workflow Management
Ideal for use cases that require complex business workflows, such as processes involving multiple departments or services. - Coordinated Transactions
Supports distributed transaction coordination, ensuring that all related operations either complete successfully or are rolled back.
Advantages
- Centralized Coordination Logic
The coordination logic is centralized within the orchestrator, simplifying the implementation of individual services. - Workflow Visibility
Enhances the understanding and visualization of complete business workflows. - Transaction Management
Enables the coordination of distributed transactions across heterogeneous services.
Challenges
- Tight Coupling
It can introduce tight coupling between services, as they must adhere to the orchestrator’s format and expectations. - Single Point of Failure
The orchestrator becomes a single point of failure and scalability bottleneck. - Increased Complexity
It can add overall architectural complexity, especially as workflows grow in size and complexity.
Technologies
There are multiple scenarios where the orchestrator pattern can be implemented in software development. In this case, we have Apache Camel, AWS Step Functions, and various tools from the Spring framework, such as Spring Cloud Data Flow, Spring Cloud Task, Spring Cloud Stream, Spring Integration, Spring Batch, and Camel Spring Boot.
Scenario example
Let’s see how we could implement the previously mentioned “scenario example” step by step: “Purchase Process in an E-commerce”.
- User places an order: An OrderService receives the purchase request.
- Inventory validation: The orchestrator coordinates the invocation of an InventoryService to check product availability.
- Payment processing: It then coordinates with a PaymentService to process the payment.
- Product shipping: Finally, it coordinates with a ShippingService to arrange the shipment.
- Confirmation and status update: After receiving confirmations from all services, the orchestrator updates the order status and notifies the user.
Service definition
OrderService:
public class OrderService {
public String placeOrder(String productId) {
// Logic for placing an order
return "Order placed for product: " + productId;
}
}
InventoryService:
public class InventoryService {
public String checkAvailabiity(String orderId) {
// Logic to validate product availability in the order
return "Inventory checked for order: " + orderId;
}
}
PaymentService:
public class PaymentService {
public String processPayment(String orderId) {
// Logic for pay proccessing
return "Payment processed for order: " + orderId;
}
}
ShippingService:
public class ShippingService {
public String shipOrder(String orderId) {
// Logic to order shipping
return "Order shipped for order: " + orderId;
}
}
Orchestrator Definition
OrderOrchestrator:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderOrchestrator {
private final OrderService orderService;
private final PaymentService paymentService;
private final ShippingService shippingService;
@Autowired
public OrderOrchestrator(OrderService orderService, PaymentService paymentService, ShippingService shippingService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.shippingService = shippingService;
}
public String processOrder(String productId) {
// Step 1: Place order
String orderId = orderService.placeOrder(productId);
// Step 2: Process payment
String paymentResult = paymentService.processPayment(orderId);
// Step 3: Ship order
String shippingResult = shippingService.shipOrder(orderId);
// Return results
return "Order Processing Summary:\n" +
"Order ID: " + orderId + "\n" +
"Payment Result: " + paymentResult + "\n" +
"Shipping Result: " + shippingResult;
}
}
This simple example aims to simulate an e-commerce purchase scenario, where an OrderOrchestrator coordinates order placement, payment processing, and order shipment. It would be necessary to include a notifier to inform the user and, of course, implement the logic, validations, error handling, etc.
Service Choreography
The choreography pattern in microservices architecture is a decentralized approach to coordinating interactions between services.
Instead of relying on a central orchestrator to control the workflow, each microservice collaborates with others by emitting and responding to events.
Each service is autonomous and decides how to act based on the events it receives, leading to looser coupling between services.
Key Characteristics
- Decentralization
In choreography, there is no central component (orchestrator) controlling the workflow. - Event-Driven Communication
Communication between services is primarily event-based. A service emits an event when something significant happens, and other services can react to these events. - Well-Suited for Distributed Systems
Choreography is particularly suitable for distributed systems, where services can be geographically dispersed and scale independently.
Advantages
- Lower Coupling
Choreography reduces coupling between microservices since each one does not need direct knowledge of the internal details of others. Services only need to know which events to emit and how to react to specific events. - Scalability
By decentralizing coordination, a choreographed architecture scales more efficiently. Each service can scale independently according to its own needs without relying on a central orchestrator. - Flexibility
Service independence allows for easier changes and updates in each component. Services can be added, modified, or removed without directly impacting others, as long as events remain compatible. - Independent Development
Separate development teams can work on specific services without interference. The event interface defines how services communicate, enabling faster and parallel development.
Challenges
- Tracking Complexity
As the number of services and events increases, tracking and understanding the overall workflow can become complex. Maintaining visibility of the entire process can be challenging, especially in distributed environments. - Event Ordering
Ensuring the correct sequence of events can be difficult. In some cases, it is crucial that specific events are processed in a particular order, which may require additional management mechanisms. - Error Handling
Managing errors is more challenging in a choreographed environment. Identifying and handling failures may require a robust strategy to maintain system integrity and consistency. - Difficult for Complex Workflows
For highly structured business workflows, choreography can become hard to maintain and understand. In such cases, a central orchestrator may be a better fit to coordinate and control the process efficiently.
Technologies
To implement the choreography architecture pattern, a tool capable of handling events is required. Three commonly used tools for this purpose are Apache Kafka, RabbitMQ, or even Amazon SNS (Simple Notification Service).
Example of Choreography with Kafka in Spring
This example uses Apache Kafka as the messaging system to facilitate choreography between microservices. Each service emits events to a Kafka topic, and a Kafka consumer in another service processes those events. This approach enables asynchronous and decentralized communication between microservices in an event-driven system.
- Kafka Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.KafkaTemplate;
@Configuration
@EnableKafka
public class KafkaConfig {
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
// Additional configuration
// ...
}
- Services (with producer):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
public OrderService(KafkaTemplate<String, Object> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void placeOrder(String productId) {
// Lógica para colocar la orden
String orderId = generateOrderId();
// Event emission to Kafka
kafkaTemplate.send("order-placed", new OrderPlacedEvent(orderId));
}
private String generateOrderId() {
// Lógica para generar un ID de orden
return "ORD123";
}
}
PaymentService:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
public PaymentService(KafkaTemplate<String, Object> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void processPayment(String orderId) {
// Logic for proccessing payment
// Event emission to Kafka
kafkaTemplate.send("payment-processed", new PaymentProcessedEvent(orderId));
}
}
ShippingService:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class ShippingService {
private final KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
public ShippingService(KafkaTemplate<String, Object> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void shipOrder(String orderId) {
// Logic for order shipping
// Event emission to Kafka
kafkaTemplate.send("order-shipped", new OrderShippedEvent(orderId));
}
}
- Consumer:
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class KafkaEventConsumer {
@KafkaListener(topics = "order-placed")
public void handleOrderPlacedEvent(OrderPlacedEvent event) {
// Logic to handle order placed event
System.out.println("Order Placed Event Received for Order ID: " + event.getOrderId());
}
@KafkaListener(topics = "payment-processed")
public void handlePaymentProcessedEvent(PaymentProcessedEvent event) {
// Logic to handle payment proccessed event
System.out.println("Payment Processed Event Received for Order ID: " + event.getOrderId());
}
@KafkaListener(topics = "order-shipped")
public void handleOrderShippedEvent(OrderShippedEvent event) {
// Logic to handle order shipped event
System.out.println("Order Shipped Event Received for Order ID: " + event.getOrderId());
}
}
In this example, each service emits events to a corresponding Kafka topic, and the Kafka consumer (KafkaEventConsumer) in the same or another service handles those events.
This approach illustrates how microservices collaborate in a decentralized manner through events in a Kafka-based system. Each microservice remains independent and reacts to events without the need for a central orchestrator.
Orchestration vs. Choreography

In orchestration, a controller (orchestrator) manages the flow of interactions between services. Each service request is executed according to a specific order and condition, with the orchestrator deciding the flow based on configuration and the outcomes of other services.
In choreography, all services operate independently and interact solely through shared events and flexible connections. Services subscribe only to the events relevant to them and perform a specific task when they receive a notification.
In event-driven architectures, the most popular pattern is choreography. However, depending on your requirements, it may not always be the best option.
When deciding which pattern to choose, it is important to consider these technical aspects (though they are not the only ones):
- Workflow complexity: For simpler and more structured workflows, orchestration may be more appropriate. For complex and dynamically changing workflows, choreography can offer greater flexibility.
- Visibility and control: If centralized control and visibility are required, orchestration may be preferable. If a more distributed and decentralized architecture is desired, choreography might be the better choice.
- Scalability: Choreography often facilitates scalability because services can evolve more independently. Orchestration can face scalability challenges in highly distributed service environments.
Ultimately, the choice between orchestration and choreography depends on the nature of the system, specific requirements, and design preferences. Some architectures may even combine both patterns depending on the specific needs of different workflows.
Using Orchestration and Choreography together
Orchestration and choreography are not mutually exclusive and can work together within a microservices architecture. In many cases, both approaches can be implemented simultaneously, with certain operations being orchestrated while others rely on event-driven choreography.
A hybrid approach often provides a balanced and flexible solution, allowing for centralized control where needed while maintaining the scalability and decoupling benefits of an event-driven system. This combination is sometimes referred to as “orchestrated choreography” or “choreography guided by orchestration”, where orchestration oversees the broader workflow while individual services communicate via events to handle specific tasks asynchronously.
Some of the advantages of combining orchestration and choreography are:
- Flexibility and scalability. Choreography provides flexibility and scalability for autonomous interactions, while orchestration ensures centralized control where necessary.
- Reduced coupling. Services can remain autonomous and loosely coupled, making changes and updates easier.
However, there are also challenges:
- Design complexity. Careful architectural planning is required to determine which parts of the system should be managed through orchestration and which should rely on choreography.
- Coordination. Ensuring proper synchronization between orchestrated and choreographed components may require additional effort and monitoring.
Key considerations
- Clarity in roles. Clearly defining the roles and responsibilities of both orchestration and choreography components is essential to avoid ambiguity and inefficiencies.
- Transaction management. Ensuring consistency across services requires a well-defined transaction handling strategy that accommodates both orchestration and choreography.
- Monitoring and tracking. A robust monitoring system is crucial for tracking and analyzing service behavior, especially in distributed environments where visibility can be challenging.
- Adaptability. The combination of orchestration and choreography should be flexible enough to adapt as system requirements and characteristics evolve over time.
Example: In an online purchase process, orchestration could handle cart verification and payment processing, while choreography could be used for inventory management and notification handling.
The decision to use both approaches depends on system complexity, business requirements, and the development team’s preferences. In scenarios where both centralized control and distributed autonomy are needed, this hybrid approach can offer the best of both worlds.
Both the orchestration and choreography patterns are closely related to the SAGA pattern, which we will explore in the next article.
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.