CQRS pattern in microservices

Srikanth Dannarapu
Javarevisited
Published in
4 min readMar 13, 2023

CQRS (Command Query Responsibility Segregation) is a design pattern that suggests separating read and write operations into separate models to improve scalability, performance, and maintainability of the system. In a microservices architecture, CQRS can be applied to separate the command side (write operations) from the query side (read operations) of the system.

An example of using CQRS in a Spring Boot microservices architecture could be for an e-commerce application. Let’s say that the application has a feature where users can search for products.

In this case, the Command side would handle the creation, updating, and deletion of products. This would involve services that allow users to add products to the system, update product details, and delete products from the system.

On the other hand, the Query side would be responsible for handling user requests for product information. This would involve services that allow users to search for products based on various criteria, such as name, category, price range, etc. The Query side would be optimized for fast read operations, and the data would be stored in a denormalized form to improve query performance.

To implement this using CQRS in a Spring Boot microservices architecture, we could have separate microservices for the Command and Query sides. The Command microservice would handle product creation, updates, and deletions, and it would publish events to a Kafka topic whenever a product is created, updated, or deleted.

The Query microservice would subscribe to this Kafka topic and update its own data store with the latest product information. It would expose a set of REST endpoints that allow users to search for products based on various criteria. The Query microservice could use Elasticsearch or another search engine to provide fast search capabilities.

By separating the Command and Query sides, we can optimize each side for its specific use case. The Command side can focus on ensuring data consistency and enforcing business rules, while the Query side can focus on providing fast, responsive queries to users.

Command side:

On the Command side, we can have a Spring Boot microservice that handles the creation, updating, and cancellation of orders. We can use Spring Data JPA to interact with the database and Apache Kafka to publish events when orders are created, updated, or cancelled. Here’s an example of how the service might look like:

@Service
public class OrderService {

@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;

@Autowired
private OrderRepository orderRepository;

public void createOrder(Order order) {
// Validate the order

// Save the order to the database
orderRepository.save(order);

// Publish an event to Kafka
OrderEvent event = new OrderEvent(order.getId(), OrderEventType.CREATED);
kafkaTemplate.send("orders", event);
}

public void updateOrder(Order order) {
// Validate the order

// Save the order to the database
orderRepository.save(order);

// Publish an event to Kafka
OrderEvent event = new OrderEvent(order.getId(), OrderEventType.UPDATED);
kafkaTemplate.send("orders", event);
}

public void cancelOrder(Long orderId) {
// Get the order from the database
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));

// Cancel the order
order.setStatus(OrderStatus.CANCELLED);

// Save the order to the database
orderRepository.save(order);

// Publish an event to Kafka
OrderEvent event = new OrderEvent(order.getId(), OrderEventType.CANCELLED);
kafkaTemplate.send("orders", event);
}
}

Query Side:

@Service
public class OrderQueryService {

@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;

@Autowired
private OrderRepository orderRepository;

@PostConstruct
public void init() {
// Subscribe to the orders topic
kafkaTemplate.subscribe("orders", this::handleOrderEvent);
}

private void handleOrderEvent(ConsumerRecord<String, OrderEvent> record) {
OrderEvent event = record.value();

switch (event.getEventType()) {
case CREATED:
case UPDATED:
// Update the order in the data store
Order order = orderRepository.findById(event.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(event.getOrderId()));
order.setStatus(event.getEventType() == OrderEventType.CREATED ?
OrderStatus.CREATED : OrderStatus.UPDATED);
orderRepository.save(order);
break;
case CANCELLED:
// Delete the order from the data store
orderRepository.deleteById(event.getOrderId());
break;
default:
throw new IllegalArgumentException("Invalid order event type: " + event.getEventType());
}
}

public List<Order> searchOrders(OrderSearchCriteria criteria) {
// Use Spring Data JPA to search for orders in the data store
// Return a list of orders that match the search criteria
}
}

By separating the Command and Query sides, we can optimize each side for its specific use case. The Command side can focus on ensuring data consistency and enforcing business rules, while the Query side can focus on providing fast, responsive queries to clients.

In the above example, it is not necessary for OrderService and OrderQueryService to connect to the same database.

The OrderService would typically connect to a transactional database where it performs create/update/delete operations on the Order entity. On the other hand, the OrderQueryService would typically connect to a read-only database (which could be a replica of the transactional database) or an event store that stores the events related to orders. This read-only database is optimized for fast queries to retrieve data to be displayed to users.

Sequence diagram:

The client places an order through the API Gateway. The API Gateway publishes the order to the OrderEventTopic, which is consumed by the OrderService. The OrderService saves the order to the OrderRepository and publishes an OrderCreated event to the OrderEventTopic. The API Gateway then queries the OrderQueryService to get the status of the order. The OrderQueryService queries the OrderProjectionRepository to get the status of the order, which is then returned to the API Gateway and finally to the user.

Sequence diagram

--

--