Instead of using the same model for both, CQRS maintains separate models optimized for writing and reading data independently.

Why Do We Need CQRS?
In a traditional application, the same domain model and database handle both read and write operations. As applications grow, however, these workloads often have very different requirements.Consider a Spring Boot e-commerce application. When a customer places an order, the application validates the request, reserves inventory, processes payment, applies business rules, and stores the order.
Displaying the customer's order history, however, simply retrieves data that has already been stored.
Write operations focus on maintaining business rules and data consistency, whereas read operations require fast retrieval and may combine data from multiple tables for dashboards, reports, or search.
Using the same model for both workloads often leads to compromises because a schema optimized for transactional updates is typically not optimized for complex queries.
CQRS solves this by separating the write model from the read model, allowing each to be optimized, scaled, and evolved independently.
CQRS Architecture
CQRS separates the application into two independent models: one responsible for processing Commands and another for handling Queries.Commands
A Command performs an operation that changes the application's state, such as creating, updating, or deleting data.Commands execute business logic, validate requests, enforce business rules, and update the write database.
They typically return only the operation's outcome, such as success, failure, or the identifier of the created resource.
Suppose a customer places an order.
POST /orders
Since this operation modifies application state, the request is handled by the Command side.
@PostMapping("/orders")
public void createOrder(@RequestBody CreateOrderRequest request) {
commandService.createOrder(request);
}
The command service validates the request, reserves inventory, processes payment if required, and stores the order in the write database.
Write Database
The write database is responsible for storing the application's transactional data and enforcing business rules.It is typically implemented using a relational database such as PostgreSQL, MySQL, or Oracle, where data is stored in a normalized schema to minimize redundancy and maintain consistency.
For example, an e-commerce application may store orders across multiple related tables.
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
order_date TIMESTAMP NOT NULL,
status VARCHAR(20) NOT NULL
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
CREATE TABLE payments (
payment_id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
Creating an order typically involves multiple transactional operations.
BEGIN;
INSERT INTO orders (...);
INSERT INTO order_items (...);
INSERT INTO payments (...);
COMMIT;
After the transaction commits successfully, the command side publishes an OrderCreated event so that other components can update their own data.
Queries
A Query retrieves information without modifying application state.Queries are optimized for fast data retrieval and never modify data. They often use a read database or read model designed specifically for search, dashboards, and reporting.
Later, when the customer views the order, the request is handled by the Query side.
GET /orders/101
@GetMapping("/orders/{id}")
public OrderView getOrder(@PathVariable Long id) {
return queryService.findOrder(id);
}
The query service retrieves the order from the read database. Unlike the command side, it does not execute business logic or modify application state.
Read Database
The read database is optimized for fast queries rather than transactional updates.Unlike the write database, it often stores denormalized data so that a single query can retrieve all the information required by the application without performing multiple joins.
For example, data from multiple transactional tables may be combined into a single denormalized read model.
CREATE TABLE order_view (
order_id BIGINT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL,
payment_status VARCHAR(20) NOT NULL,
item_count INT NOT NULL
);
Retrieving an order now requires only a simple lookup.
SELECT *
FROM order_view
WHERE order_id = 101;
The read database is updated asynchronously by consuming events published from the command side.
Because synchronization is asynchronous, it may temporarily lag behind the write database, resulting in eventual consistency.
Synchronizing the Read Model
In many CQRS implementations, the write database is a transactional relational database such as PostgreSQL, while the read database is a denormalized store such as Apache Cassandra, optimized for dashboards and fast queries.Suppose a customer places an order. The command side stores the order in PostgreSQL and, after the transaction commits successfully, publishes an OrderCreated event.

For example, one dashboard displays the latest orders, while another shows sales by customer.
Latest Orders Widget
CREATE TABLE latest_orders (
order_id BIGINT,
customer_name TEXT,
order_date TIMESTAMP,
total_amount DECIMAL,
status TEXT,
PRIMARY KEY (order_id)
);
Customer Sales Widget
CREATE TABLE customer_sales (
customer_id BIGINT,
customer_name TEXT,
total_orders INT,
total_spent DECIMAL,
last_order_date TIMESTAMP,
PRIMARY KEY (customer_id)
);
When users open the dashboard, each widget retrieves data directly from its own read model.
SELECT *
FROM latest_orders
LIMIT 20;
SELECT *
FROM customer_sales
WHERE customer_id = 101;
Instead of executing multiple joins and aggregations on the transactional database, each query reads directly from a denormalized table optimized for that specific use case.
Because these read models are updated asynchronously from events, they may temporarily lag behind the transactional database.
This temporary delay is known as eventual consistency and is an accepted trade-off for improved scalability and query performance.
When Should You Use CQRS?
CQRS is most beneficial when read and write workloads have significantly different requirements.It is well suited for applications where writes involve complex business rules, while reads require highly optimized queries, dashboards, reports, or search capabilities.
The write model focuses on transactional consistency, while the read model can use denormalized views or even different storage technologies to deliver fast query performance.
When Should It Be Avoided?
CQRS introduces additional architectural complexity by requiring separate command and query models, synchronization mechanisms, and often separate databases.Since the read model is typically updated asynchronously, applications must also tolerate eventual consistency, where queries may temporarily return older data until the read model is updated.
For simple CRUD applications with modest traffic and straightforward business logic, these additional components usually provide little benefit while increasing development and operational overhead.
Event sourcing is a software architecture pattern where state changes are saved as a sequence of immutable, append-only events rather than overwriting current data.
Summary
The CQRS (Command Query Responsibility Segregation) pattern separates data modification operations from data retrieval operations by maintaining independent command and query models.This separation allows the write side to focus on transactional consistency while the read side is optimized for fast queries, reporting, dashboards, and search.
CQRS is commonly used in large-scale microservices and is often combined with Apache Kafka, Event-Driven Architecture, Event Sourcing, and the Saga Pattern to build scalable, event-driven distributed systems.