Developing robust yet maintainable software solutions remains at the heart of modern software development. The Command Query Responsibility Segregation (CQRS) pattern offers an efficient method by creating a clear separation between executing commands and querying data, simplifying the system architecture and improving performance. At the same time, the Data-Oriented Programming (DOP) approach strongly focuses on efficiently handling data. This article shows how integrating 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. It goes back to the Command-Query Separation (CQS) principle introduced initially 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 a system’s state 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 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. However, as we will see in the article, this apparent disadvantage can also be a great advantage regarding 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. It is important to note that this is irrelevant 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:
- Immutability
Data structures are immutable, meaning they cannot be changed once created. - Separation of identity and state
In DOP, a data item’s identity is separated from its state. This means that an object’s state at a given point in time is simply a snapshot of its data, making its history and changes over time easier to understand. - Data modeling as a central design element
In contrast to OOP, which focuses on objects’ behavior and methods, 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.
In his article [2], Brian Goetz discusses the implementation of DOP in Java 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
ava has introduced several significant new language features in recent versions that significantly affect how developers write and structure code. The new features important for this article are records, sealed classes, and pattern matching, described below.
Records were introduced in Java 16 as a preview feature and have been integral to the language since Java 17. Records are a special class type 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. A developer can explicitly control which other classes or interfaces can inherit from this type by sealing a class or interface. This is achieved by the sealed keyword and the permitted keywords, which specify the exact types allowed to inherit from the sealed class. Sealed classes promote more precise control over inheritance and enable 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. 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 but also with a switch statement or expression whether an object belongs to a specific 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, allowing the order to be made and changed. Listing 1 shows an implementation that I often encounter in practice. 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 unclear which data the interface changes. In addition, too much data is transferred because the entire order is always sent from the client to the server, which is 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 positively affects 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). The example uses a sealed interface implemented by all commands to group them and further improve comprehensibility. Thanks to the sealed interface, we can use the switch expression’s exhaustiveness to implement the commands’ handling. Exhaustiveness means that the compiler checks whether all values have been handled. Firstly, this is a significant 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 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. This example 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 in [3].
Conclusion
Java has been constantly evolving to keep pace with technological changes. 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 using CQRS with the separation of responsibilities makes it easier to understand and maintain and greatly benefits 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