Site icon Vinsguru

CQRS Pattern With Spring Boot

cqrs pattern

Overview:

In this tutorial, I would like to demo CQRS Pattern with Spring Boot which is one of the Microservice Design Patterns to independently scale read and write workloads of an application & have well optimized data schema.

CQRS Pattern:

1. Read vs Write Models:

Most of the applications are CRUD in nature. When we design these applications, we create entity classes and corresponding repository classes for CRUD operations. We use the same model classes for all the CRUD operations. However these applications might have completely different READ and WRITE requirements! 

For example: Let’s consider an application in which we have 3 tables as shown here.

All these tables have been normalized. Creating a new user or a product or an order go to the appropriate tables quickly and directly. But if we consider READ requirements, we would not simply want all the users, products, just orders. Instead we would be interested in knowing all the orders details of an user, state wise total sale, state and product wise sale etc. A lot of aggregate information which involves multiple tables join. All these join READ operations might require corresponding DTO mapping as well.

More normalization we do, it is more easier to write and but it is more difficult to read which in turn affects the overall read performance.

Even though we say WRITE is easy, before we insert the record into the database, we might do business validation. All these logic might be present in the model class. So we might end up creating a very complex model which is trying to support both READ and WRITE.

An application can have completely different READ and WRITE requirements. So a model created for WRITE might not work for READ. To solve this problem, we can have separate models for READ and WRITE.

2. Read vs Write Traffic:

Most of the web based applications are read heavy. Let’s take Facebook/Twitter for example. Whether we post any new updates or not, we all check these applications very often in a day. Of course, we keep getting updates in those applications which are due to some inserts in the DB. But it is a lot of READ than WRITE. Also, consider a flight-booking application. Probably less than 5% of the users might book a ticket while majority of the application users would keep searching for the best flight meeting their needs.

Applications have more requests for READ operations compared to WRITE operations. To solve this problem, We can even have separate Microservices for READ and WRITE. So that they can be scaled in/out independently depends on their needs. 

This is what CQRS Pattern is about which stands for Command Query Responsibility Segregation Pattern.

That is separating Command (write) and Query (read) models of an application to scale read and write operations of an application independently. We can solve above 2 problems using CQRS Pattern. Lets see how it can be implemented.

Sample Application:

Lets consider a simple application in which we have 3 services as shown below. (Ideally all these services should have different databases. Here just for this article, I am using same db). We are interested only in the order-service related functionalities for now for this article. 

We have 3 tables for this application as shown here.

CREATE TABLE users(
   id serial PRIMARY KEY,
   firstname VARCHAR (50),
   lastname VARCHAR (50),
   state VARCHAR(10)
);

CREATE TABLE product(
   id serial PRIMARY KEY,
   description VARCHAR (500),
   price numeric (10,2) NOT NULL
);

CREATE TABLE purchase_order(
    id serial PRIMARY KEY,
    user_id integer references users (id),
    product_id integer references product (id),
    order_date date
);

Lets assume that we have an interface for the order service for the read and write operations as shown below.

public interface OrderService {
    void placeOrder(int userIndex, int productIndex);
    void cancelOrder(long orderId);
    List<PurchaseOrderSummaryDto> getSaleSummaryGroupByState();
    PurchaseOrderSummaryDto getSaleSummaryByState(String state);
    double getTotalSale();
}

CQRS Pattern – Read & Write Interfaces:

Instead of having 1 single interface which is responsible for all the READ and WRITE operations, Lets split them into 2 different interfaces as shown here.

Order Query Service:

public interface OrderQueryService {
    List<PurchaseOrderSummaryDto> getSaleSummaryGroupByState();
    PurchaseOrderSummaryDto getSaleSummaryByState(String state);
    double getTotalSale();
}

Order Command Service:

public interface OrderCommandService {
    void createOrder(int userIndex, int productIndex);
    void cancelOrder(long orderId);
}

CQRS Pattern – Read & Write Implementations:

Once we have separated command and query interfaces, Lets implement the operations.

@Service
public class OrderQueryServiceImpl implements OrderQueryService {

    @Autowired
    private PurchaseOrderSummaryRepository purchaseOrderSummaryRepository;

    @Override
    public List<PurchaseOrderSummaryDto> getSaleSummaryGroupByState() {
        return this.purchaseOrderSummaryRepository.findAll()
                .stream()
                .map(this::entityToDto)
                .collect(Collectors.toList());
    }

    @Override
    public PurchaseOrderSummaryDto getSaleSummaryByState(String state) {
        return this.purchaseOrderSummaryRepository.findByState(state)
                        .map(this::entityToDto)
                        .orElseGet(() -> new PurchaseOrderSummaryDto(state, 0));
    }

    @Override
    public double getTotalSale() {
        return this.purchaseOrderSummaryRepository.findAll()
                        .stream()
                        .mapToDouble(PurchaseOrderSummary::getTotalSale)
                        .sum();
    }

    private PurchaseOrderSummaryDto entityToDto(PurchaseOrderSummary purchaseOrderSummary){
        PurchaseOrderSummaryDto dto = new PurchaseOrderSummaryDto();
        dto.setState(purchaseOrderSummary.getState());
        dto.setTotalSale(purchaseOrderSummary.getTotalSale());
        return dto;
    }
}

Command vs Query –  Controllers:

Overall the project structure looks like this. If you take a closer look at this, You could notice that I have 2 different controllers for Order Service. 

We have dedicated controllers for Query (READ) and Command (WRITE). 

We can even control if the app should work in READ mode or WRITE mode based on a property value.

Order Query Controller:

My order query controller is implemented as shown here which has only GET requests. It does not do anything which modifies the data. 

@RestController
@RequestMapping("po")
@ConditionalOnProperty(name = "app.write.enabled", havingValue = "false")
public class OrderQueryController {

    @Autowired
    private OrderQueryService orderQueryService;

    @GetMapping("/summary")
    public List<PurchaseOrderSummaryDto> getSummary(){
        return this.orderQueryService.getSaleSummaryGroupByState();
    }

    @GetMapping("/summary/{state}")
    public PurchaseOrderSummaryDto getStateSummary(@PathVariable String state){
        return this.orderQueryService.getSaleSummaryByState(state);
    }

    @GetMapping("/total-sale")
    public Double getTotalSale(){
        return this.orderQueryService.getTotalSale();
    }

}

Order Command Controller:

@RestController
@RequestMapping("po")
@ConditionalOnProperty(name = "app.write.enabled", havingValue = "true")
public class OrderCommandController {

    @Autowired
    private OrderCommandService orderCommandService;

    @PostMapping("/sale")
    public void placeOrder(@RequestBody OrderCommandDto dto){
        this.orderCommandService.createOrder(dto.getUserIndex(), dto.getProductIndex());
    }

    @PutMapping("/cancel-order/{orderId}")
    public void cancelOrder(@PathVariable long orderId){
        this.orderCommandService.cancelOrder(orderId);
    }
}

App properties:

My application.yaml looks like this to run this application in READ mode. 

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/productdb
    username: vinsguru
    password: admin
app:
  write:
    enabled: false

Of course, we can change the below property to run the application in WRITE mode.

app.write.enabled=true

CQRS Pattern – Scaling:

Now we have successfully split the READ and WRITE models. Now we need the ability to scale our system independently. Lets see how we can achieve that.

Creating Spring Bean At Run Time:

On the Query and Command controllers, I have added a condition for Spring Boot whether to create this controller or not. That is, below annotation will help creating the controller only if the app.write.enabled is set to true. Otherwise It would not create this controller bean.

//for WRITE controller
@ConditionalOnProperty(name = "app.write.enabled", havingValue = "true")

If the value is set to false, It will create this controller. 

//for READ controller
@ConditionalOnProperty(name = "app.write.enabled", havingValue = "false")

So based on a property, we change if the app is going to behave like a read-only node or write-only node. It gives us the ability to run multiple instances of an app with different modes. I can have 1 instance of my app which does the writing while I can have multiple instances of my app just for serving the read requests. They can be scaled in-out independently. We can place them behind a load balancer / proxy like nginx – so that READ / WRITE requests could be forwarded to appropriate instances using path based routing or some other mechanism. 

 

Command vs Query DB:

In the above example we had used the same DB. We can even go one level further by having separating databases for READ and WRITE as shown here. That is, any write operations will push the changes to the read database via event sourcing

CQRS Pattern brings additional benefits like using different DBs for the same application with well optimized data schema. However do note that data will be eventually consistent in this approach! As we can run multiple instances of read nodes with its own DB, we can even place them in different regions to provide user experience with minimal latency.

Summary:

Other than easy maintenance and scaling benefits, CQRS Pattern provides few other benefits like parallel development. That is, 2 different developers/team could work together on a Microservice. When one person could work on Query side while other person can work on the Command side. 

Read more about Microservice Design Patterns.

The source code is available here

Happy learning 🙂

Share This:

Exit mobile version