Skip to content

CQRS Pattern

Command Query Responsibility Segregation (CQRS) separates read operations (queries) from write operations (commands) to optimize performance and scalability.

What is CQRS?

CQRS stands for Command Query Responsibility Segregation. It's a pattern that separates the model used to update information (commands) from the model used to read information (queries).

In traditional architectures, the same model serves both reads and writes. CQRS splits these concerns, allowing each side to be optimized independently. Commands change state but don't return data. Queries return data but don't change state.

Core Concepts

Commands vs Queries

Commands: Operations that change state. Examples: CreateProduct, UpdateProduct, ProcessOrder. Commands are imperative ("do this") and typically return success/failure status.

Queries: Operations that return data without changing state. Examples: GetProduct, ListOrders, SearchProducts. Queries are interrogative ("tell me about this") and return data.

Separation of Models

Commands and queries can use different data models, optimized for their specific use cases. The write model focuses on business logic and validation. The read model focuses on efficient data retrieval and presentation.

Independent Scaling

Since reads and writes are separated, they can be scaled independently. If your application is read-heavy, you can scale the query side without affecting the command side.

In This Workshop

Our backend module (spring-boot-kafka-backend) implements CQRS at the service layer with dedicated service classes:

Command Services (Write Operations)

ProductCommandService
- createProduct(ProductDTO)
- updateProduct(UUID id, ProductDTO)
- deleteProduct(UUID id)
- updateStock(UUID id, int quantity)

OrderCommandService
- createOrder(PurchaseOrderDTO)
- processOrder(UUID orderId)
- updateOrderStatus(UUID orderId, PurchaseOrderStatus status)

Query Services (Read Operations)

ProductQueryService
- getAllProducts(): List<ProductDTO>
- getProductById(UUID id): ProductDTO
- searchProducts(String keyword): List<ProductDTO>
- getProductsByCategory(String category): List<ProductDTO>

OrderQueryService
- getAllOrders(): List<PurchaseOrderDTO>
- getOrderById(UUID id): PurchaseOrderDTO
- getOrdersByCustomer(String customerId): List<PurchaseOrderDTO>
- getOrdersByStatus(PurchaseOrderStatus status): List<PurchaseOrderDTO>

Simplified CQRS

Our implementation uses the same MySQL database for both commands and queries. This is a simplified CQRS pattern suitable for learning and moderate-scale applications.

Benefits of CQRS

  • Scalability: Scale read and write operations independently based on load
  • Optimization: Optimize each side for its specific use case (write validation vs read performance)
  • Separation of Concerns: Clear boundaries between state changes and data retrieval
  • Flexibility: Different data models for commands and queries if needed
  • Security: Easier to apply different security rules to commands vs queries

Advanced CQRS

In more complex scenarios, CQRS can use completely separate databases:

  • Write Database: Optimized for transactions, normalization, and consistency (e.g., PostgreSQL)
  • Read Database: Optimized for queries, denormalization, and speed (e.g., Elasticsearch, MongoDB)
  • Synchronization: Event-driven sync using Kafka to propagate changes from write to read models

This advanced pattern enables polyglot persistence - using the right database for each job.

Key Takeaways

  • Separate command (write) operations from query (read) operations
  • Commands change state, queries return data without side effects
  • Enables independent scaling and optimization of each side
  • Can use separate data models or even separate databases for advanced scenarios