Interview contents
CQRS (Command Query Responsibility Segregation) Pattern
Introduction
In traditional CRUD-based systems, the same data model is used for both reads (queries) and writes (commands). While simple, this approach struggles at scale:
- Queries often require complex joins or aggregations.
- Writes may need strict validation and transactional guarantees.
- The same model ends up overcomplicated, trying to satisfy both needs.
The CQRS Pattern (Command Query Responsibility Segregation) addresses this by separating read and write responsibilities.
- Commands: Modify state (writes).
- Queries: Retrieve state (reads).
This separation simplifies models, improves performance, and works well with event-driven systems.
Intent
The intent of the CQRS Pattern is to separate command (write) and query (read) responsibilities into distinct models to improve scalability, clarity, and performance.
Structure
Core Components
-
Command Model
- Handles state changes.
- Enforces business rules.
-
Query Model
- Handles data retrieval.
- Optimized for reads (e.g., denormalized views).
-
Command Handlers
- Execute commands (e.g., placeOrder).
-
Query Handlers
- Execute queries (e.g., getOrderHistory).
graph TD A[Client] -->|Command| B[Command Handler] A -->|Query| C[Query Handler] B --> D["(Write Model)"] C --> E["(Read Model)"]✅ Separation of concerns.
✅ Optimized for both read and write paths.
Participants
-
Commands
- Represent user intent to change state.
- Example:
PlaceOrderCommand.
-
Command Handlers
- Validate and apply business logic.
- Persist changes to write model.
-
Queries
- Represent data retrieval requests.
- Example:
GetOrderHistoryQuery.
-
Query Handlers
- Fetch from read-optimized model.
Collaboration Flow
- Client issues command → Command Handler → Write Model.
- Client issues query → Query Handler → Read Model.
- (Optional) Event sourcing or replication keeps read model updated.
Implementation in Java
Command
public class PlaceOrderCommand { private final String orderId; private final double total; public PlaceOrderCommand(String orderId, double total) { this.orderId = orderId; this.total = total; } public String getOrderId() { return orderId; } public double getTotal() { return total; }}Command Handler
@Servicepublic class PlaceOrderHandler { private final OrderRepository repository; public PlaceOrderHandler(OrderRepository repository) { this.repository = repository; }
public void handle(PlaceOrderCommand cmd) { if(cmd.getTotal() <= 0) throw new IllegalArgumentException(); repository.save(new Order(cmd.getOrderId(), cmd.getTotal())); }}Query
public class GetOrderQuery { private final String orderId; public GetOrderQuery(String orderId) { this.orderId = orderId; } public String getOrderId() { return orderId; }}Query Handler
@Servicepublic class GetOrderHandler { private final JdbcTemplate jdbc; public GetOrderHandler(JdbcTemplate jdbc) { this.jdbc = jdbc; }
public Order handle(GetOrderQuery query) { return jdbc.queryForObject("SELECT * FROM orders WHERE id = ?", new Object[]{query.getOrderId()}, (rs, rowNum) -> new Order(rs.getString("id"), rs.getDouble("total"))); }}✅ Write model handles validation.
✅ Read model optimized for queries.
Consequences
Benefits
- Optimized Models – Separate read/write models tailored for their purpose.
- Scalability – Independent scaling of read vs write workloads.
- Clarity – Commands express intent, queries express retrieval.
- Event Sourcing Friendly – Commands emit events, queries read projections.
- Security – Easier to enforce different authorization on commands vs queries.
Drawbacks
- Complexity – More moving parts than CRUD.
- Consistency – Eventual consistency between write and read models.
- Learning Curve – Developers must understand commands, queries, events.
Real-World Case Studies
1. Banking Systems
- Commands enforce strict rules for transactions.
- Queries optimized for statements/history.
2. E-commerce Platforms
- Order placement (command).
- Order history lookup (query).
3. Event-Sourced Systems
- Commands emit events.
- Read models updated asynchronously for queries.
Extended Java Case Study
Traditional CRUD (Monolithic)
@RestController@RequestMapping("/orders")public class OrderController { @Autowired private OrderRepository repo;
@PostMapping public void placeOrder(@RequestBody Order order) { repo.save(order); }
@GetMapping("/{id}") public Order getOrder(@PathVariable String id) { return repo.findById(id).orElseThrow(); }}❌ Same model for reads/writes.
❌ Hard to optimize queries without breaking writes.
CQRS Approach
PlaceOrderCommandhandled byPlaceOrderHandler.GetOrderQueryhandled byGetOrderHandler.- Separation allows different optimizations.
✅ Clean separation of responsibilities.
Interview Prep
Q1: What is CQRS?
Answer: A pattern that separates read (query) and write (command) responsibilities into different models and handlers.
Q2: What are pros and cons of CQRS?
Answer: Pros: optimized models, scalability, clarity. Cons: complexity, eventual consistency.
Q3: How does CQRS relate to event sourcing?
Answer: CQRS works well with event sourcing: commands produce events, read models consume them.
Q4: When should you use CQRS?
Answer: For complex domains with different read/write workloads. Avoid in simple CRUD apps.
Q5: Give a real-world example.
Answer: Banking: money transfer = command, account balance lookup = query.
Visualizing CQRS Pattern
graph TD Client -->|Command| CH[Command Handler] Client -->|Query| QH[Query Handler] CH --> WM["(Write Model)"] QH --> RM["(Read Model)"]✅ Clear separation of paths.
Key Takeaways
- CQRS Pattern separates read and write models.
- Commands modify state, queries fetch state.
- Benefits: scalability, optimized models, clarity.
- Drawbacks: complexity, eventual consistency.
- Works well with event sourcing and microservices.
Next Lesson
Next, we’ll dive into Event Sourcing Pattern — persisting events instead of state, often combined with CQRS for full power.
Continue to Event Sourcing Pattern →