JVM Advent

The JVM Programming Advent Calendar

CQRS meets modern Java

The development of powerful yet maintainable software solutions remains at the heart of modern software development. The Command Query Responsibility Segregation (CQRS) pattern offers an efficient method for this by creating a clear separation between executing commands and querying data, which simplifies the system architecture and improves performance. At the same time, the Data-Oriented Programming (DOP) approach brings a strong focus on the efficient handling of data. This article shows how the integration of CQRS and DOP leads to more robust, scalable, and easier-to-maintain systems.

Introduction to CQRS

Command Query Responsibility Segregation (CQRS) is a pattern first described by Greg Young [1]. It is based on the principle of separation of responsibilities and goes back to the Command-Query Separation (CQS) principle originally introduced by Bertrand Meyer in his book “Object-Oriented Software Construction”. While CQS states that methods should either be commands that change the state of an object but do not return a value, or queries that return a value but do not change the state, CQRS extends this principle to the architectural level of software applications.

CQRS separates the responsibility for processing commands that change the state of a system from the responsibility for querying information about this state. This separation enables an optimized design of both areas of responsibility, which can lead to better structuring. By modeling commands and queries separately, developers can also implement more complex business logic more clearly and easily.

However, introducing CQRS into a system can increase its complexity because two separate models have to be managed. As we will see in the course of the article, however, this apparent disadvantage can also be a great advantage in terms of data queries and performance. It can also lead to easier maintainability because changes to the query functionality can be made independently of the command logic, and vice versa. In addition, the separation enables optimized scaling because read and write operations place different demands on system resources and can therefore be scaled independently of each other. It is important to note here that this is not relevant for many business applications because the number of users and user behavior is known in advance.

Data-Oriented Programming

Data-oriented programming (DOP) is a paradigm that offers an alternative approach to traditional object-oriented programming (OOP) by focusing on the data and its structures rather than the objects and their behavior. The key concepts of DOP are:

  1. Immutability
    Data structures are immutable, meaning that they cannot be changed once they are created.
  2. Separation of identity and state
    In DOP, the identity of a data item is separated from its state. This means that the state of an object at a given point in time is simply a snapshot of its data, making its history and changes over time easier to understand.
  3. Data modeling as a central design element
    In contrast to OOP, where the focus is on the behavior and methods of objects, DOP focuses on the design of the data models. This leads to a clear structuring of the data and makes data manipulation and querying easier.

Brian Goetz discusses the implementation of DOP in Java in his article [2] and describes how the combination of the new Java features, records, sealed classes, and pattern matching supports the DOP principles and leads to more precise, readable, and reliable programs. In particular, the commands from CQRS are modeled according to Brian Goetz’s ideas.

Modern Java Features

Java has introduced several important new language features in recent versions that significantly affect the way developers write and structure code. The new features that are important for this article are records, sealed classes, and pattern matching which are described below.

Records were introduced in Java 16 as a preview feature and have been an integral part of the language since Java 17. Records are a special type of class that is used to model simple data structures, so-called data carriers, with minimal code. A record automatically creates all fields as final and generates getters, but without a get prefix, for these fields, as well as appropriate implementations of equals(), hashCode(), and toString(). Records are therefore ideal for modeling immutable data objects and significantly reduce the amount of code to be written.

Sealed classes were officially introduced in Java 17 and offered a way to restrict inheritance. By sealing a class or interface, a developer can explicitly control which other classes or interfaces can inherit from this type. This is achieved by the sealed keyword together with the permitted keywords, which specify the exact types that are allowed to inherit from the sealed class. Sealed classes promote more precise control over inheritance and allow developers to define and secure hierarchical-type systems more precisely, which is particularly useful in domain modeling.

Pattern matching for the instanceof operator was introduced in Java 16 as a preview feature and has been further developed since then. It enables a more compact and readable way of performing type queries and subsequent type conversions. With pattern matching, you can not only check in an if and now also with a switch statement or expression whether an object belongs to a certain type, but if there is a match, convert it directly to a local variable of the corresponding type. This simplifies the code by removing the need to perform an explicit type conversion in a separate step and thus reduces the error rate when handling type conversions. Finally, Java 21 also adds record patterns, which allow direct access to individual components of a record.

CQRS Commands with modern Java

To illustrate the integration of CQRS with modern Java and the new features mentioned above, we will focus on the commands. An example is a web shop that offers standard functions such as “Create order”, “Add item” and “Change quantity”. In a traditional approach, REST interfaces would be created for this, with which the order can be created and changed. Listing 1 shows an implementation that I often encounter in practice. Basically, the entire order is always transferred, regardless of which data has changed. The PurchaseOrderDTO is also a one-to-one copy of the PurchaseOrder entity and can therefore be easily mapped using a mapper such as ModelMapper or MapStruct.

@ResponseStatus(HttpStatus.CREATED)
@PostMapping
void post(@RequestBody PurchaseOrderDTO purchaseOrderDTO) {

  var purchaseOrder = modelMapper.map(purchaseOrderDTO, 
                                      PurchaseOrder.class);

  customerRepository.findById(purchaseOrder.getCustomer().getId())
    .ifPresent(purchaseOrder::setCustomer);

  purchaseOrderRepository.save(purchaseOrder);
}

@PutMapping("{id}")
void put(@PathVariable Long id, @RequestBody PurchaseOrderDTO purchaseOrderDTO) {

  if (id.equals(purchaseOrderDTO.getId())) {
    throw new IllegalArgumentException();
  }
 
  var purchaseOrder = modelMapper.map(purchaseOrderDTO, PurchaseOrder.class);

  purchaseOrderRepository.save(purchaseOrder);
}

This design presents several problems. In the put() method, it is not clear which data is changed by the interface. In addition, too much data is transferred because the entire order is always sent from the client to the server, which is completely unnecessary in most cases. On the client side, it is unclear which data in the interface object can be changed.

CQRS helps us solve these problems by sending commands from the client to the server instead of objects. The commands can be derived from the requirements at the beginning of the section: «Create order», «Add item» and «Change quantity». Focusing on commands has a positive effect on understanding the application because it adds semantics to the code.

sealed interface OrderCommand permits CreateOrder, AddOrderItem, UpdateQuantity {

  record CreateOrder(long customerId) implements OrderCommand {}

  record AddOrderItem(long orderId, long productId, int quantity) implements OrderCommand {}

  record UpdateQuantity(long orderItemId, int quantity) implements OrderCommand {}

}

Since commands are immutable, it is a good idea to model them as Java records (Listing 2). To group them and further improve comprehensibility, the example uses a sealed interface that is implemented by all commands. Thanks to the sealed interface, we can use the exhaustiveness of the switch expression to implement the handling of the commands. Exhaustiveness means that the compiler checks whether all values ​​have been handled. Firstly, this is a big advantage compared to an if/else if/else construct, and secondly, when you add a new command during further development, you are informed if it is not processed.

switch (orderCommand) {

  case OrderCommand.CreateOrder(long customerId) -> {
    var purchaseOrder = orderService.createOrder(customerId);                
    return created(...).buildAndExpand(...).toUri()).build();
  }

  case OrderCommand.AddOrderItem(long orderId, long productId, int quantity) -> {
    var orderItemRecord = orderService.addItem(orderId, productId, quantity);
    return created(...).buildAndExpand(...).toUri()).build();
  }

  case OrderCommand.UpdateQuantity(long orderItemId, int quantity) -> {
    orderService.updateQuantity(orderItemId, quantity);
    return ok().build();
  }
}

In Listing 3, in addition to the use of the switch expression, you can also see a use case for pattern matching with record patterns. The commands CreateOrder, AddOrderItem, and UpdateQuantity are deconstructed and the individual fields are passed directly to the OrderService. In this example, this has the advantage that the OrderService has no knowledge of the commands and thus remains independent. The entire source code of the examples can be found at [3].

Conclusion

Java has been constantly evolving to keep pace with changes in the technology world. In recent versions, Java has introduced several modern language features such as records, pattern matching, and sealed classes. These extensions not only improve the readability and writeability of the code but also enable more functional programming approaches and improved data modeling that help Java developers write more efficient and expressive programs.
The article has shown that the use of CQRS with the separation of responsibilities makes it easier to understand and maintain and benefits greatly from the new Java language features, especially on the command side of implementation.

Links

[1] Greg Young (2010): CQRS Documents by Greg Young
https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
[2] Brian Goetz (2022): Data Oriented Programming in Java, InfoQ https://www.infoq.com/articles/data-oriented-programming-java/
[3] https://github.com/simasch/cqrs-meets-modern-java

Author: Simon Martinelli

Simon Martinelli is a Java Champion, Vaadin Champion, and Oracle ACE Pro, with over three decades of experience as a software architect, developer, consultant, and trainer. As the owner of Martinelli LLC, he specializes in optimizing full-stack development with Java and has a deep focus on modern architectures and distributed systems. He frequently shares his expertise by speaking at international conferences, writing articles, and maintaining his blog: https://martinelli.ch. His passion for teaching is reflected in his work as a lecturer at the Bern University of Applied Sciences BFH and the University of Applied Sciences Northwestern Switzerland FHNW, where he teaches courses on modern architecture, distributed systems, persistence technologies, and DevOps.

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén