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

Advantages

Challenges

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”.

  1. User places an order: An OrderService receives the purchase request.
  2. Inventory validation: The orchestrator coordinates the invocation of an InventoryService to check product availability.
  3. Payment processing: It then coordinates with a PaymentService to process the payment.
  4. Product shipping: Finally, it coordinates with a ShippingService to arrange the shipment.
  5. 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

Advantages

Challenges

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.

  1. 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
    // ...
}
  1. 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));
    }
}
  1. 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

Source: AWS
Source: AWS

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):

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:

However, there are also challenges:

Key considerations

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.

Tell us what you think.

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.

Subscribe