A Comprehensive Guide to Implementing CQRS with Spring Boot
In software design, CQRS (Command Query Responsibility Segregation) is a useful technique for addressing the requirements of complex applications. In particular, it separates read and write operations to improve performance and make the system easier to manage. In this article, we walk through how to implement CQRS with Spring Boot.
What Is CQRS?
Command Query Responsibility Segregation (CQRS) is a design pattern that separates the read and write operations of a data store. Instead of using the same model to update and query data, CQRS divides the application into two parts
- Command side: Handles writes (create, update, delete).
- Query side: Handles reads.
This separation lets you optimize and scale each side independently.
Benefits of CQRS
- Scalability: Read and write operations can be scaled independently according to their respective loads.
- Performance: Performance can be optimized with models specialized for reads and writes.
- Maintainability: A clear separation of concerns improves code readability and maintainability.
- Flexibility: Changes or optimizations on one side can be introduced easily without affecting the other.
When to Use CQRS
- Complex domains: When there are complex and varied operations on the data
- High read/write load: Systems with an unbalanced read/write load
- Event-driven systems: Applications that can benefit from event sourcing
Designing the Command Side
Handling write operations
The Command side is responsible for handling all write operations. It processes commands that express an intent to change the system state.
Key components
- Command model: Represents the data structure of incoming commands.
- Controller: Exposes endpoints through which clients can send commands.
- Service: Contains the business logic for processing commands.
- Repository: Interacts with the data store to persist changes.
"Create Order" example
Let's build an order management system.
1. Command model
Define a CreateOrderCommand that represents the data needed to create an order.
// CreateOrderCommand.java
public class CreateOrderCommand {
private String product;
private int quantity;
private BigDecimal price;
}2. Entity
Define the Order entity that will be persisted in the database.
// Order.java
public class Order {
private Long id;
private String product;
private int quantity;
private BigDecimal price;
private LocalDateTime orderDate;
}3. Repository
Create an OrderRepository for data persistence.
// OrderRepository.java
public interface OrderRepository extends JpaRepository<Order, Long> {
}4. Service
Implement an OrderCommandService to handle the business logic.
// OrderCommandService.java
public class OrderCommandService {
private final OrderRepository orderRepository;
public Order createOrder(CreateOrderCommand command) {
Order order = new Order();
order.setProduct(command.getProduct());
order.setQuantity(command.getQuantity());
order.setPrice(command.getPrice());
order.setOrderDate(LocalDateTime.now());
return orderRepository.save(order);
}
}5. Controller
Declare the endpoints inside OrderCommandController.
// OrderCommandController.java
public class OrderCommandController {
private final OrderCommandService orderCommandService;
public ResponseEntity<Order> createOrder( CreateOrderCommand command) {
Order order = orderCommandService.createOrder(command);
return new ResponseEntity<>(order, HttpStatus.CREATED);
}
}Designing the Query Side
Handling read operations
The Query side is optimized for reading data. It can have its own model and database optimized for queries
Key components
- Query model: A data structure optimized for read operations
- Controller: Exposes endpoints through which clients can query data
- Service: Contains the business logic for retrieving data
- Repository: Interacts with the data store to fetch data
"Find Order" example
1. Query model
Define an OrderView DTO (Data Transfer Object) for the response.
// OrderView.java
public class OrderView {
private Long id;
private String product;
private int quantity;
private BigDecimal price;
private LocalDateTime orderDate;
}2. Repository
Since this example uses the same database, we reuse OrderRepository.
3. Service
Implement an OrderQueryService for data retrieval.
// OrderQueryService.java
public class OrderQueryService {
private final OrderRepository orderRepository;
public List<OrderView> getAllOrders() {
List<Order> orders = orderRepository.findAll();
return orders.stream()
.map(this::convertToView)
.collect(Collectors.toList());
}
public OrderView getOrderById(Long id) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
return convertToView(order);
}
private OrderView convertToView(Order order) {
OrderView view = new OrderView();
view.setId(order.getId());
view.setProduct(order.getProduct());
view.setQuantity(order.getQuantity());
view.setPrice(order.getPrice());
view.setOrderDate(order.getOrderDate());
return view;
}
}4. Controller
Declare the endpoints inside OrderQueryController.
// OrderQueryController.java
public class OrderQueryController {
private final OrderQueryService orderQueryService;
public ResponseEntity<List<OrderView>> getAllOrders() {
List<OrderView> orders = orderQueryService.getAllOrders();
return ResponseEntity.ok(orders);
}
public ResponseEntity<OrderView> getOrderById( Long id) {
OrderView order = orderQueryService.getOrderById(id);
return ResponseEntity.ok(order);
}
}Integrating Event Sourcing
Event sourcing is often used together with CQRS and involves storing changes to application state as a sequence of events.
Benefits of event sourcing
- Audit trail: A complete history of changes
- Snapshots: The ability to restore system state at any point in time
- Asynchronous processing: Separation of event handling from command handling
Implementing event sourcing
1. Define events
Create event classes that represent changes.
// OrderCreatedEvent.java
public class OrderCreatedEvent {
private Long orderId;
private String product;
private int quantity;
private BigDecimal price;
private LocalDateTime orderDate;
}2. Event store
Implement a mechanism for the event store.
// EventStore.java
public interface EventStore {
void saveEvent(String aggregateId, Object event);
List<Object> getEvents(String aggregateId);
}3. Publish events
Modify OrderCommandService to publish events.
public class OrderCommandService {
private final OrderRepository orderRepository;
private final EventStore eventStore;
public Order createOrder(CreateOrderCommand command) {
Order order = new Order();
// ... set order properties
orderRepository.save(order);
// Publish event
OrderCreatedEvent event = new OrderCreatedEvent();
// ... set event properties
eventStore.saveEvent(order.getId().toString(), event);
return order;
}
}4. Event handlers
Create handlers to process the events.
// OrderEventHandler.java
public class OrderEventHandler {
public void on(OrderCreatedEvent event) {
// Handle the event (e.g., update read models, send notifications)
}
}Challenges and Considerations
Complexity
- Increased complexity: CQRS can add complexity to the system architecture.
- Eventual consistency: The read model may not reflect writes immediately.
When not to use CQRS
- Simple applications: For straightforward CRUD applications, CQRS may be overkill.
- Small teams: The added complexity can be hard for a small development team to manage.
Best practices
- Modular design: Keep the Command and Query sides modular.
- Consistent models: Ensure that models are consistent and well defined.
- Testing: Test both sides thoroughly and independently..
Implementing the CQRS pattern with Java Spring Boot brings major benefits in application scalability and maintainability, but it also introduces additional complexity into the system, so implementation and migration can be challenging.
Here we introduce a model-based way to implement this demanding CQRS concept in Spring Boot more intuitively and simply.
The screen above shows a simple product ordering domain modeled with the EventStorming model generation feature of MSAEZ (www.msaez.io), a microservices architecture design tool.
EventStorming is a methodology for designing systems by visualizing domain events; the model above uses a simple online-shop ordering domain as its example.
A user can place or cancel a product order, which produces an OrderPlaced event and an OrderCanceled event respectively. When these events are published, the delivery service can pick them up to start a delivery, or cancel the delivery for a canceled order. If a delivery starts, the product management side decreases the stock quantity and publishes a StockDecreased event, completing the process.
In microservices, CQRS can be applied from two main perspectives.
First, the command model (blue sticky) that is managed transactionally within a microservice can be replicated as-is and used as a query model. When a single database receives a large volume of read requests, the model is replicated and pushed down so that the command model's performance can be reserved for transactions.
Second, in the distributed environment of a microservices architecture, the data unique to each service is collected and projected together. For example, the order information inside the order bounded context and the delivery information from the delivery bounded context can both be received so that changes to the data are stored.
In an EventStorming model made up of domain events, aggregates, and query models (commands), the way to visualize the CQRS pattern is with the green ReadModel sticky.
Inside the ReadModel you can specify various query types, including CQRS, and list the data that can be handled, stored, and updated within it. Here the ReadModel has been named MyPage and its data entered.
Through this ReadModel, you can build a query model for a single service that receives many reads, and, given the special nature of MSA as a distributed platform, provide a model on the dashboard that gathers the data needed when a user clicks MyPage, guaranteeing read performance. This corresponds to the second of the two ways of applying CQRS in microservices presented above.
Once all the data is entered, first configure the CREATE clause so that, among the events received based on the model, the initial OrderPlaced event is used to match the data values inside MyPage to the data carried in that event.
Then, for changes to data values, enter the event and data information in the UPDATE clause so that the added and changed data can be stored. Also, add an entry in the DELETE clause so that a delete query runs when the OrderCanceled event is published for a canceled order.
After completing the CQRS settings this way, the code generated with Code Generate, the core feature of MSAEZ, looks like this.
package stmalldemo.infra;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Service;
import stmalldemo.config.kafka.KafkaProcessor;
import stmalldemo.domain.*;
public class MyPageViewHandler {
//<<< DDD / CQRS
private MyPageRepository myPageRepository;
public void whenOrderPlaced_then_CREATE_1(
OrderPlaced orderPlaced
) {
try {
if (!orderPlaced.validate()) return;
// create view object
MyPage myPage = new MyPage();
// set the event's values on the view object
myPage.setOrderId(String.valueOf(orderPlaced.getId()));
myPage.setUserId(orderPlaced.getUserId());
myPage.setProductName(orderPlaced.getProductName());
myPage.setProductId(orderPlaced.getProductId());
myPage.setQty(String.valueOf(orderPlaced.getQty()));
myPage.setAddress(orderPlaced.getAddress());
myPage.setStatus(orderPlaced.getStatus());
// save to the view repository
myPageRepository.save(myPage);
} catch (Exception e) {
e.printStackTrace();
}
}
public void whenDeliveryStarted_then_UPDATE_1(
DeliveryStarted deliveryStarted
) {
try {
if (!deliveryStarted.validate()) return;
// look up view objects
List<MyPage> myPageList = myPageRepository.findByDeliveryId(
String.valueOf(deliveryStarted.getId())
);
for (MyPage myPage : myPageList) {
// set the event's eventDirectValue on the view object
myPage.setOrderId(String.valueOf(deliveryStarted.getOrderId()));
myPage.setProductId(deliveryStarted.getProductId());
myPage.setProductName(deliveryStarted.getProductName());
myPage.setAddress(deliveryStarted.getAddress());
myPage.setStatus(deliveryStarted.getStatus());
// save to the view repository
myPageRepository.save(myPage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void whenOrderCanceled_then_DELETE_1(
OrderCanceled orderCanceled
) {
try {
if (!orderCanceled.validate()) return;
// delete query on the view repository
myPageRepository.deleteByOrderId(
String.valueOf(orderCanceled.getId())
);
} catch (Exception e) {
e.printStackTrace();
}
}
//>>> DDD / CQRS
}
As shown, MSAEZ automatically generates the Spring Boot code that implements the CQRS pattern based on the model. You can see it create the object and repository, and store the DirectValue carried by each event in the repository, ready for use.
When an update or delete event is published, it looks up the object, changes the corresponding data values, or runs a delete query.
Implementing the CQRS pattern with Java Spring Boot greatly improves the scalability and maintainability of an application, and by separating Command and Query responsibilities, each side can be optimized for its own requirements.
Adopting CQRS in large or complex systems brings many benefits, but it also increases system complexity, so it is worth evaluating your application's requirements and constraints before adopting CQRS.