A few weeks ago, we started a series of blog posts on microservices architecture patterns, beginning with an introduction to what they are, followed by their organization and structure, and then exploring communication and coordination between microservices.

Now, we continue this series on microservices architecture patterns with the fourth installment, where we will further explore patterns that address communication and coordination challenges between microservices.

We still have several more patterns to cover in this category, but we’ll start with the SAGA pattern, as it is closely related to the patterns we’ve recently discussed: service orchestration and service choreography.

Next, we’ll move on to Event Sourcing and EDA (Event-Driven Architecture), leaving CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend), and Outbox for the final section.

We recall one of the key advantages of microservices architectures: their modularity. This means that each service within the software can be built independently. Additionally, once they are running, each service operates using its own process, ensuring better scalability, flexibility, and maintainability.

SAGA

The SAGA pattern is used in distributed systems to manage long-running and complex transactions efficiently and consistently. This pattern takes inspiration from the narrative structure of literary sagas, where a series of interconnected events form a coherent story.

In the field of computing, the SAGA pattern enables the coordination and execution of multiple operations across various services, ensuring global transaction consistency, even in distributed environments and failure scenarios.

Key Features

Advantages of the SAGA Pattern

Challenges of the SAGA Pattern

The SAGA pattern provides an effective solution for managing distributed transactions in complex environments, such as e-commerce. While it offers numerous advantages, its implementation can be challenging and requires careful design to ensure system consistency and reliability.

saga pattern
  1. Reserving Products in Inventory

When a user adds products to their shopping cart, a transaction is initiated to reserve those products in the inventory. If the inventory has sufficient stock, the reservation is confirmed. If not, a failure occurs, triggering a compensation step to release any reserved items.

  1. Processing Payment

Once the products are reserved, the payment process begins. A transaction is executed to charge the customer the total purchase amount. If the payment transaction is successful, the process moves to the next step. In case of failure, a compensation step is triggered to release the product reservations in the inventory.

  1. Generating the Invoice

After the payment is successfully processed, an invoice is generated for the customer. This step involves creating a transaction record in the database and generating an invoice document. If the invoice generation is successful, the process moves to the final step. Otherwise, a compensation step is executed to revert the payment transaction and release the product reservations in the inventory.

  1. Sending Shipping Notification

Finally, the customer is notified that their order has been shipped. This action involves updating the order status and sending a notification via email or text message to the customer. If the notification is successfully sent, the purchase process is completed. In case of failure, a compensation step is triggered to revert the invoice generation, payment processing, and inventory reservations.

  1. Automatic Compensation

At each step of the purchase process, a compensation mechanism is defined to reverse operations in case of failure. For example, if the payment processing fails, the compensation mechanism releases the product reservations in the inventory, ensuring the system’s integrity.

If we translate this example into code using Java and Spring Boot, in a schematic way and without completing the logic, it would be:

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public boolean processOrder(Order order) {
        // Logic to reserve products in inventory
        // If there is enough stock, proceed with the reservation
        // If not, throw an exception
        // In case of an error, an automatic rollback will be triggered

        return true;
    }
}
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    public boolean processPayment(Order order) {
        // Logic to process the payment
        // If the payment is successfully processed, return true
        // If there is a failure, throw an exception
        // In case of an error, a rollback will be automatically performed 

        return true;
    }
}
import org.springframework.stereotype.Service;

@Service
public class BillingService {

    public boolean processBilling(Order order) {
        // Logic to generate the invoice
        // If the invoice is successfully generated, return true
        // If there is a failure, throw an exception
        // In case of an error, a rollback will be automatically performed 

        return true;
    }
}
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    public boolean sendNotification(Order order) {
        // Logic to notify the customer about the shipment
        // If the notification is successfully sent, return true
        // If there is a failure, throw an exception
        // In case of an error, a rollback will be automatically performed 

        return true;
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PurchaseController {

    private final OrderService orderService;
    private final PaymentService paymentService;
    private final BillingService billingService;
    private final NotificationService notificationService;

    @Autowired
    public PurchaseController(OrderService orderService, PaymentService paymentService,
                              BillingService billingService, NotificationService notificationService) {
        this.orderService = orderService;
        this.paymentService = paymentService;
        this.billingService = billingService;
        this.notificationService = notificationService;
    }

    @PostMapping("/purchase")
    public ResponseEntity<String> processPurchase(@RequestBody Purchase purchase) {
        try {
            // Step 1: Reserve products in inventory
            orderService.reserveProducts(purchase);
            // Step 2: Process payment
            paymentService.processPayment(purchase);
            // Step 3: Generate invoice
            billingService.generateInvoice(purchase);
            // Step 4: Notify shipment
            notificationService.notifyShipping(purchase);

            return ResponseEntity.ok("Purchase processed successfully.");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body("Error processing purchase: " + e.getMessage());
        }
    }
}

In this case, we allow the Spring framework to handle the rollback automatically, as it provides built-in transaction management.

API Gateway

The API Gateway pattern serves as a single entry point for managing client requests in a microservices system. It provides functionalities such as routing, authentication, authorization, and data aggregation, simplifying client interactions and improving efficiency.

Key Features

Advantages

Challenges of the API Gateway Pattern

An example

Suppose we have a product service and a cart service in our e-commerce system. The API Gateway could expose an e-commerce endpoint that directs requests to the corresponding services.

@RestController
@RequestMapping("/ecommerce")
public class ApiGatewayController {

    @Autowired
    private ProductService productService;

    @Autowired
    private CartService cartService;

    @GetMapping("/products/{productId}")
    public Product getProduct(@PathVariable String productId) {
        // Routing to the product service
        return productService.getProductById(productId);
    }

    @GetMapping("/cart/{userId}")
    public Cart getCart(@PathVariable String userId) {
        // Routing to the cart service
        return cartService.getUserCart(userId);
    }

    @PostMapping("/cart/{userId}/add")
    public ResponseEntity<String> addToCart(@PathVariable String userId, @RequestBody CartItem item) {
        // Routing and calling the cart service to add an item
        cartService.addToCart(userId, item);
        return ResponseEntity.ok("Item added to the cart.");
    }
}
API Gateway
  1. Routing

The API Gateway handles client routes and redirects them to the corresponding microservices.

  1. Authentication and Authorization

It can integrate authentication and authorization logic to protect the underlying microservices.

@GetMapping("/user/{userId}")
public ResponseEntity<User> getUser(@PathVariable String userId, @RequestHeader("Authorization") String token) {
    // Token verification logic and call to the user service
    if (isValidToken(token)) {
        return ResponseEntity.ok(userService.getUserById(userId));
    } else {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}
  1. Data Aggregation

For complex requests, it can call multiple microservices and aggregate the results before sending a response to the client.

@GetMapping("/user/{userId}/details")
public ResponseEntity<UserDetails> getUserDetails(@PathVariable String userId) {
    // Logic to call the user service and order service, then combine the data
    User user = userService.getUserById(userId);
    List<Order> orders = orderService.getOrdersByUserId(userId);
    UserDetails userDetails = new UserDetails(user, orders);
    return ResponseEntity.ok(userDetails);
}
  1. Simplification for Clients

Clients interact with a single entry point, simplifying development and improving maintainability. The API Gateway enhances the client experience by offering combined services, preventing clients from having to call multiple endpoints to retrieve related information.

  1. Scalability

It enables independent scaling of services based on demand, as clients interact only with the API Gateway.

  1. Version Management
@GetMapping(value = "/products/{productId}", headers = "API-Version=1")
public ResponseEntity<ProductV1> getProductV1(@PathVariable String productId) {
    // Logic to call the product service with version 1 logic
    return ResponseEntity.ok(productService.getProductV1(productId));
}

@GetMapping(value = "/products/{productId}", headers = "API-Version=2")
public ResponseEntity<ProductV2> getProductV2(@PathVariable String productId) {
    // Logic to call the product service with version 2 logic
    return ResponseEntity.ok(productService.getProductV2(productId));
}
  1. API Documentation
// Using tools like Springfox to generate Swagger documentation
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.ecommerce"))
                .paths(PathSelectors.any())
                .build();
    }
}

This is a simple example of how to implement an API Gateway in Java. However, in real-world applications, it is more common to use specialized systems with API Management tools that handle security, routing, scalability, and other critical aspects efficiently. Some of the most widely used API Management tools include:

Service Discovery

The service discovery pattern is essential in microservices architecture, enabling services to dynamically find and communicate with each other without relying on static configurations.

How It Works

  1. Service Registration
  1. Service Discovery
  1. Dynamic Updates
service discovery

Key Components:

Advantages of Service Discovery

  1. Auto-Configuration

Enables services to automatically adapt to infrastructure changes, such as the addition or removal of service instances.

  1. Dynamic Scalability

Facilitates dynamic scaling, allowing services to scale horizontally without requiring manual configuration changes in other services.

  1. High Availability

If a service instance fails, the service registry updates the information, and consumers can automatically redirect their requests to available instances.

  1. Continuous Deployment

Simplifies continuous deployment, as new services can register automatically, and consumers can discover them without manual intervention.

Challenges of Service Discovery

  1. Discovery Latency

There may be slight latency when searching for services, especially in large distributed systems.

  1. Consistency

Maintaining consistency in the service registry can be challenging in highly distributed environments.

  1. Security

Security measures must be implemented to protect registry information and ensure the integrity of communications between services.

  1. Added Complexity

Introduces an additional layer of complexity to the overall architecture, requiring a balance between its benefits and operational overhead.

Example of Service Discovery with Eureka (Spring Cloud)

@SpringBootApplication
@EnableDiscoveryClient
public class ProductServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/placeOrder/{productId}")
    public String placeOrder(@PathVariable String productId) {
        // Uses the registered service name ("product-service") instead of a direct URL
        String productInfo = restTemplate.getForObject("http://product-service/product/" + productId, String.class);
        return "Order placed for Product ID " + productId + ". Product Info: " + productInfo;
    }
}

Final Considerations:

The service discovery pattern is crucial for microservices architectures, as it enables flexible and dynamic interaction between services.

However, it must be carefully implemented and managed to address potential challenges and ensure reliability and security in a production environment.

Additionally, in managed PaaS environments, this functionality is often provided out of the box.

Conclusions

In summary, we have seen how SAGA helps manage distributed transactions, API Gateway acts as a single entry point for requests, and Service Discovery enables dynamic service location.

This highlights the importance of adopting efficient strategies in distributed environments.

These tools provide robust solutions to address challenges such as scalability, availability, and reliability, while also enhancing system adaptability and maintainability. Hope you find this useful!

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