Do you know that? You start a new project with a clean domain model. Your Order entity is perfectly normalized, the database relationships are cleanly defined (3rd normal form), and saving an order is lightning fast. Everything feels right.
But then come the requirements for the front end: “We need a dashboard that shows all orders - filtered by status, aggregated by product category, including customer data and delivery address, and please with full-text search.”
You begin to bend your beautiful entities. You write monster SQL queries with seven joins. You add indexes until the disk glows. Nevertheless, the database groans under the load of read accesses, and writes suddenly become slow because indexes need to be updated.
Welcome to the “CRUD trap.”
The problem is not your database. The problem is that we are trying to use a single model for two completely opposite tasks. The solution to this is radical but effective: CQRS.
The Problem: The “God Model” Dilemma
In classic CRUD applications (Create, Read, Update, Delete) we often have a single model (e.g. the JPA entity Order) that has to do everything.
- Write (Command): Must keep data consistent, validate rules (“Is there enough money?”) and store in a normalized manner. What counts here is ACID (atomicity, consistency, isolation, durability).
- Read (Query): Wants to display data as quickly as possible, often denormalized, flat and “join-free”. Performance counts here.
These requirements fundamentally contradict each other. A model that is perfectly optimized for writing is often terrible for reading - and vice versa.
The solution: separation of table and bed
CQRS stands for Command Query Responsibility Segregation. The idea is simple: we divide our application into two logical halves.

- The Command Side:
- Task: Change data.
- Input: “Commands” (Imperative commands like
CreateOrderCommand,ShipItemCommand). - Database: Highly normalized (e.g. PostgreSQL), optimized for transactions.
- Return: Nothing (
void) or just an ID. No data!
- Input: “Commands” (Imperative commands like
- The Query Side:
- Task: Read data.
- Input: “Queries” (Questions like
GetDashboardDataQuery). - Database: Denormalized (e.g. Elasticsearch, MongoDB or a flat SQL table), optimized for fast reads (“Read Model”).
- Return: DTOs (Data Transfer Objects) that look exactly as the frontend needs them.
- Input: “Queries” (Questions like
How does the data stay in sync?
Now you’re probably wondering: “If I have two models (or even two databases), how does the data get from A to B?”
This brings us full circle to our previous articles on Event-Driven Architecture: Events.
The process looks like this:
- User sends
CreateOrderCommand. - The Command Service validates, saves in the SQL DB and fires an
OrderCreatedEvent(e.g. via RabbitMQ or Kafka). - The Query Service (or a “Projector”) listens for this event.
- It takes the data from the event and updates its own, optimized reading model (e.g. a document in Elasticsearch).
This means: Our reading model is always a few milliseconds behind. This is called Eventual Consistency.
Code example: CQRS with Spring Boot
Let’s take a look at what this looks like in code. We say goodbye to the monstrous OrderService and split the logic.
1. The Command Side (Writing)
This is where the “hard” work of business logic happens.
@Service
@Transactional
public class CreateOrderHandler {
private final OrderRepository writeRepository; // JPA / Postgres
private final EventPublisher eventPublisher; // Unser Interface zum Message Broker
public void handle(CreateOrderCommand command) {
// 1. Validierung & Logik
if (command.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
// 2. Domain Model erzeugen & speichern (Write Model)
Order order = new Order(command.getCustomerId(), command.getItems());
writeRepository.save(order);
// 3. Event feuern für die Query Side
eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getItems()));
}
}
2. Der Synchronizer (Der Projector)
This component connects the two worlds. She listens to events and builds the reading model.
@Component
public class OrderProjector {
private final OrderViewRepository readRepository; // z.B. MongoDB oder Elasticsearch
@EventListener // oder @RabbitListener
public void on(OrderCreatedEvent event) {
// Wir bauen ein optimiertes DTO für die schnelle Anzeige (Read Model)
OrderView view = new OrderView();
view.setOrderId(event.getOrderId());
view.setSummary("Bestellung mit " + event.getItems().size() + " Artikeln");
// Wir berechnen hier schon Dinge vor, damit wir es beim Lesen nicht tun müssen!
view.setSearchTags(generateSearchTags(event));
readRepository.save(view);
}
}
3. The Query Side (Reading)
There is no complex logic here, no joins, just speed.
@Service
public class DashboardQueryHandler {
private final OrderViewRepository readRepository;
public List<OrderView> handle(GetDashboardQuery query) {
// Direkter Zugriff auf das vorbereitete Modell.
// Extrem schnell, O(1) oder einfacher Index-Lookup.
return readRepository.findByCustomerId(query.getCustomerId());
}
}
When should you use CQRS? (And when not!)
CQRS is a powerful tool, but it significantly increases the complexity of your architecture (two models, synchronization, eventual consistency).
Use CQRS if:
- You have extreme traffic and need to scale reads/writes very differently (e.g. 1000x more reads than writes).
- Your business logic when writing is very complex, but your reading views look completely different.
- You already do event sourcing or event-driven architecture anyway.
Avoid CQRS if:
- You are building a simple CRUD application.
- “Eventual consistency” is unacceptable for your use case (e.g. the user needs to see their change immediately, without delay).
Conclusion
CQRS frees your domain model from the burden of representation. It allows you to choose security (ACID) for writing and speed (NoSQL/Cache) for reading - without compromise. It’s the key to high-load systems, but like everything in software architecture: it’s not a “free lunch”. You pay with complexity but gain scalability.
![[EN] CQRS Pattern erklärt: Warum CRUD in Microservices nicht reicht](/images/CQRS-BlogHeader.jpeg)