We begin the sixth installment of this series of architecture pattern posts. If you missed any, here’s a look at the previous articles:

  1. Microservices Architecture Patterns: What Are They and What Benefits Do They Offer?
  2. Architecture Patterns: Organization and Structure of Microservices.
  3. Architecture Patterns: Communication and Coordination of Microservices.
  4. Microservices Architecture Patterns: SAGA, API Gateway, and Service Discovery.
  5. Microservices Architecture Patterns: Event Sourcing and Event-Driven Architecture (EDA).

We are concluding the section on Communication and Coordination Between Microservices by exploring CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend), and Outbox.

But the series does not end here, as there is much more content coming in future posts, where we will cover additional patterns such as those related to scalability and resource management with auto-scaling, migration, testing, security, and more.

Communication and Coordination Between Microservices

CQRS (Command Query Responsibility Segregation)

The CQRS (Command Query Responsibility Segregation) pattern is an architectural design pattern that proposes separating the responsibility of reading (query) from writing (command) in an application. This separation allows each of these operations to be optimized independently, which can lead to a more scalable, flexible, and maintainable system.

Components of the CQRS Pattern:

Features and Benefits of CQRS:

Challenges of CQRS:

In summary, the CQRS pattern is a valuable technique for improving performance, scalability, and flexibility by separating read and write operations. However, it also introduces challenges in terms of complexity and data consistency that must be carefully considered before implementation.

The diagram illustrates the following processes:

  1. A company interacts with the application by sending commands through an API. Commands include actions like creating, updating, or deleting data.
  2. The application processes the incoming command in the command layer. This involves validating, authorizing, and executing the operation.
  3. The application stores the command data in the write database.
  4. Once the command is stored in the write database, events are triggered to update the read database.
  5. The read database processes and stores the data. Read databases are designed to be optimized for specific query requirements.
  6. The company interacts with the read APIs to send queries to the read side of the application.
  7. The application processes the incoming query in the read layer and retrieves the data from the read database.

Here is a code example:

Let's assume we have a product management application where users can add new products and also query the list of available products.

First, define a command to add a new product:

public class AddProductCommand {
    private String name;
    private double price;

    // Constructor, getters, and setters
}

Then, create a controller to handle write commands:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductCommandController {

    private final ProductCommandService productCommandService;

    @Autowired
    public ProductCommandController(ProductCommandService productCommandService) {
        this.productCommandService = productCommandService;
    }

    @PostMapping("/products")
    public void addProduct(@RequestBody AddProductCommand command) {
        productCommandService.addProduct(command);
    }
}

The command service handles command logic:

import org.springframework.stereotype.Service;

@Service
public class ProductCommandService {

    public void addProduct(AddProductCommand command) {
        // Logic to add a new product
        System.out.println("New product added: " + command.getName());
        // The write operation in the database would be performed here
    }
}

To handle queries, define a DTO (Data Transfer Object) for product information:

public class ProductDTO {
    private String name;
    private double price;

    // Constructor, getters y setters
}

Create a controller to handle queries:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductQueryController {

    private final ProductQueryService productQueryService;

    @Autowired
    public ProductQueryController(ProductQueryService productQueryService) {
        this.productQueryService = productQueryService;
    }

    @GetMapping("/products")
    public List<ProductDTO> getAllProducts() {
        return productQueryService.getAllProducts();
    }
}

This example demonstrates how CQRS separates write (command) operations from read (query) operations, allowing each to be optimized and scaled independently.

BFF (Backend for Frontend)

The Backend for Frontend (BFF) pattern is an architectural approach that suggests creating specialized backends for specific frontend applications. Instead of having a single backend that serves all frontend clients' needs, the BFF pattern recommends creating multiple specialized backends, each designed to meet the specific needs of a frontend client or user interface.

Characteristics of the BFF Pattern:

Components of the BFF Pattern:

Advantages of the BFF Pattern:

Challenges of the BFF Pattern:

In summary, the BFF pattern is a useful technique to optimize the user experience by providing specialized backends tailored to each frontend client’s requirements. However, it also introduces additional complexity and overhead that must be carefully considered.

Example: Implementing BFF in Java

To illustrate the BFF pattern in Java, let's consider a scenario where we have a web application and a mobile application that share common functionalities but also have specific requirements.

We will use Spring Boot to implement specialized backends for each frontend client.

Creating a Backend for the Web Application (Web BFF):

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebBFFController {

    @GetMapping("/web/data")
    public String getWebData() {
        // Logic to retrieve web-specific data
        return "Data for the web application";
    }
}

Creating a Backend for the Mobile Application (Mobile BFF):

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MobileBFFController {

    @GetMapping("/mobile/data")
    public String getMobileData() {
        // Logic to retrieve mobile-specific data
        return "Data for the mobile application";
    }
}

Each backend is specialized to serve the specific needs of its corresponding frontend client. The web application interacts with the ”/web/data” endpoint to obtain its data, while the mobile application must invoke the ”/mobile/data” endpoint to retrieve its own.

With this configuration, each frontend client has its own specialized backend that provides the necessary services and data for its respective user interface, ensuring a more optimized user experience and maintaining clear separation between frontend and backend.

Outbox

The Outbox pattern is a technique used in distributed architectures to ensure consistency between changes in a local database and the publication of events to a messaging system such as Kafka, RabbitMQ, or similar.

This pattern is particularly useful in situations where atomicity between database writes and event publication must be ensured, such as in event-driven systems or microservices architectures.

Components of the Outbox Pattern:

Advantages of the Outbox Pattern:

Challenges of the Outbox Pattern:

In summary, the Outbox pattern is an effective technique for ensuring consistency and atomicity between local database changes and related event publication.

Although it adds implementation complexity, it provides a robust solution for event-driven systems or microservices architectures.

Example: Implementing the Outbox Pattern in Java with Kafka

Here’s a simplified example of how to implement the Outbox pattern in Java using Spring Boot and Kafka for processing a purchase event in an e-commerce system.

Defining an Entity for Purchase Events in the Outbox:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class PurchaseEvent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long userId;
    private Long productId;
    private int quantity;

    public PurchaseEvent() {}

    public PurchaseEvent(Long userId, Long productId, int quantity) {
        this.userId = userId;
        this.productId = productId;
        this.quantity = quantity;
    }

    // Getters and Setters
}

Creating a Service to Handle Purchase Event Persistence:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PurchaseEventService {

    @Autowired
    private PurchaseEventRepository purchaseEventRepository;

    public void addPurchaseEventToOutbox(Long userId, Long productId, int quantity) {
        PurchaseEvent event = new PurchaseEvent(userId, productId, quantity);
        purchaseEventRepository.save(event);
    }
}

Processing and Publishing Events from the Outbox to Kafka:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class PurchaseEventPublisher {

    @Autowired
    private PurchaseEventRepository purchaseEventRepository;
    @Autowired
    private KafkaProducer kafkaProducer;

    @Scheduled(fixedDelay = 1000) // Executes every second
    public void publishPendingPurchaseEvents() {
        List<PurchaseEvent> pendingEvents = purchaseEventRepository.findAll();
        for (PurchaseEvent event : pendingEvents) {
            kafkaProducer.send("purchaseEvent", event.toString()); // Send event to Kafka
            purchaseEventRepository.delete(event);
        }
    }
}

In this adapted example, when a POST request is made to "/purchase" with the purchase details, a purchase event is recorded in the outbox.

Then, the PurchaseEventPublisher component periodically scans the outbox and sends purchase events to Kafka for further processing.

Example of the Outbox pattern in microservices architecture
  1. The client makes a POST request to "/purchase" with the purchase details (userId, productId, quantity).
  2. The PurchaseController receives the request and calls the registerPurchaseEvent method from the PurchaseEventService.
  3. PurchaseEventService creates a new PurchaseEvent object with the purchase details and stores it in the outbox via the PurchaseEventRepository.
Example of a Purchase Event in the Outbox pattern
  1. The PurchaseEventPublisher component periodically scans the outbox for pending events.
  2. PurchaseEventPublisher finds the pending purchase event and sends it to Kafka using KafkaProducer.
  3. Kafka receives the purchase event.
  4. Then, interested consumers retrieve it for further processing.

Conclusion

In summary, effective communication and proper coordination between microservices are essential for success, though they are not enough on their own.

By understanding the different approaches and tools available, organizations can build more flexible, scalable, and robust systems that can adapt to changing market and business demands.

This concludes our discussion on microservices communication and coordination patterns. Stay tuned as we continue exploring more architectural patterns in upcoming posts.

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