CQRS implementation separates the path that changes data from the path that reads it. Commands carry intent (“place this order”, “approve this payout”) and run the rules. Queries return exactly the shape a screen, a report or a search box needs, with no obligation to look like the write model. The split is a response to a specific pain: writes that are full of policy, and reads that are slow because they keep joining the tables those policies need.
Contents
At CodeWiz we introduce that split in Laravel, Node.js and existing modular codebases. We design command handlers, query handlers and, where the history itself is the product, an event store. We also say no. A CRUD admin panel with a handful of validations does not become better because every button is renamed a command.
This service is for billing, bookings, marketplaces, inventory and multi-tenant products where a wrong state transition is expensive, and for read-heavy screens (dashboards, search, public catalogues) that are crushing a database shaped for writes. If both sides are simple, we keep one model.

Command Handlers That Enforce the Business Rules
A command is a request to do one thing, named in the language of the business: PlaceOrder, RefundPayment, SuspendTenant. A command handler loads the aggregate or record it needs, calls the behaviour that checks the rules, and saves the result. It does not build a view, and it does not return a page of data. Success is “the change was accepted”. Failure is a domain error the caller can show or retry.
What belongs in the handler, and what does not
- Validation that the command is well-formed (IDs present, amounts positive) happens before the handler, at the edge.
- Business rules (an empty order cannot be placed, a refund cannot exceed the capture) live on the model the handler calls, so a second entry point cannot skip them.
- The handler is idempotent where the business requires it. A client retry of
PlaceOrderwith the same ID must not create a second order. - Side effects that can fail independently (email, a payment capture, a search index) are recorded and handled after the state change commits, not inside the same fragile sequence.
The handler itself stays thin. This is the shape we use in PHP; the TypeScript version is the same idea with a different syntax:
final class PlaceOrderHandler
{
public function __construct(
private OrderRepository $orders,
private Clock $clock,
) {}
public function handle(PlaceOrder $command): void
{
$order = $this->orders->get($command->orderId);
$order->place($this->clock->now());
$this->orders->save($order);
}
}Because the rule sits on Order::place, tests cover it without HTTP, and a queue worker can call the same handler the API calls. Martin Fowler’s note on CQRS is the short explanation we send people before a design discussion, so the room argues about the split and not about the acronym.
Query Handlers and Read Models Built for the Screen
A query handler answers a question and changes nothing. OrderDetails, OpenInvoicesForAccount, SearchCatalog each return a read model: a DTO shaped for one consumer. The handler may run SQL, read Redis, or call Typesense. It does not load an aggregate and walk its object graph to render a table.
Read models are allowed to be boring
- One read model per screen or API response. A dashboard does not reuse the write-side repository “to stay DRY”.
- Columns are pre-joined and pre-formatted. The price the UI shows is stored as the UI needs it, currency symbol included if that is what the screen renders.
- Indexes follow the query, not the aggregate boundary. A report that filters by tenant and status gets that index even if the write model never looks up orders that way.
- Rebuilds are a feature. If a projection is wrong, we can replay the source of truth into an empty read model instead of hand-editing rows.
Search is a read model like any other. A Typesense collection fed by the write side is often a better catalogue than a pile of SQL LIKE queries, and it keeps the transactional database off the autocomplete path.

Event Stores: The Record of What Actually Happened
An event store is an append-only log of domain events, in order, per aggregate. Instead of storing only the current row, we store OrderPlaced, PaymentCaptured, LineRefunded. The current state is a fold over that history. The store is the source of truth for the write side; read models are projections built from the same events.
What we persist
- The event name in the ubiquitous language, a schema version, the aggregate ID, a monotonic version and the payload of facts, not a diff of every column “just in case”.
- Metadata that operations need: who caused it, which command ID, when it happened according to the application’s clock.
- Optimistic concurrency on the aggregate version, so two handlers cannot append over each other and silently drop a rule.
- A subscription or a polling projector that updates read models, with a checkpoint so a restart continues rather than starts over.
We reach for a dedicated store (EventStoreDB, or a PostgreSQL table designed as an append-only log) when the history is a requirement: audits, “what did this account look like on Tuesday”, or several consumers that each need their own shape. We do not introduce one because CQRS was mentioned in a meeting. The Azure architecture notes on the CQRS pattern make the same distinction: separating reads and writes does not require storing events.
Where losing a step is unacceptable, the event is the thing we refuse to skip. That is the same instinct as designing an online store so every customer action can be reconstructed, which we covered in keeping orders and customer actions recoverable.

Read/Write Separation in a CQRS Implementation
Read/write separation is a scale, not a switch. A CQRS implementation can be a naming convention inside one database, or two engines with a projector between them. We pick the cheapest level that removes the actual pain, and we leave room to step up without rewriting the commands.
| Level | What is separated | Pays off when | You take on |
|---|---|---|---|
| Logical split | Command and query classes, one database, one schema | Rules are getting lost in controllers, reads are still fine | Very little infrastructure |
| Dedicated read queries | Write model stays normalized; screens get their own SQL or views | A few pages dominate the database with joins | Two ways of reading the same facts, kept honest by tests |
| Projected read store | A projector fills Redis, Typesense or a read schema | Search, dashboards or public traffic must not hit the write database | Lag, rebuilds and monitoring of the projector |
| Event-sourced writes | The log is the write model; every read model is a projection | Audit, time travel or many independent consumers are requirements | Versioning, replay and a stricter approach to schema change |
One database until the load says otherwise
Most products we see need the first or second row. Commands and queries become different types, the write side gets smaller transactions, and the heavy screen gets a query that does not load the aggregate. A second database appears when the read load, the search requirements or the number of consumers justify operating a projector. Scaling the write database itself, by partitioning or sharding, is a separate decision we have written up in the sharding and partitioning guide. CQRS does not replace that work; it often makes the seam obvious.
Consistency, Projections and What the User Sees
The moment a read model is updated asynchronously, a user can write and then not see the write. That is not a bug to hide. It is a product decision with a budget measured in milliseconds or seconds, and a user interface that does not pretend the list is instantly complete.
- After a command, the UI can read from the write model for that one response (the order just placed) and use the projection for lists.
- A projector lag alert fires before customers notice, on the checkpoint age, not on a vague “the queue looks busy”.
- Projections are rebuildable from the source of truth in a staging run we have actually timed.
- Commands that must not be reordered (capture, then refund) carry the aggregate version, so the write side stays sequential even when readers are behind.
Commands That Touch Money
Payment flows are where a sloppy command model becomes a double charge or a missing refund. We treat capture, void and refund as commands with their own idempotency keys, and we store the provider’s reference next to the domain event. The gateway integration stays at the edge: the handler asks a port to capture, and the event records the outcome. Retries are safe because the same command ID returns the same result instead of calling the provider again. We have described the surrounding integration work in payment gateway integration for online stores.
Common CQRS Implementation Mistakes We Fix
- Command handlers that also assemble the response DTO, so the “split” is only a folder name.
- A read model nobody can rebuild, edited by hand until it contradicts the write side.
- Events named
OrderUpdatedwith the entire row inside, which cannot be versioned and means nothing to the business. - Two writes in one command (the order and the invoice and the email) with no plan for the one that fails in the middle.
- Introducing an event store, then still treating a mutable row as the source of truth.
- Assuming every query must be consistent to the millisecond, and then building synchronous projections that bring back the original coupling.
How We Work
- Discovery: we list the state transitions that carry rules and the screens that are slow or awkward to query.
- Level: we choose how far the split goes, from separate handlers to a projected read store or an event log, and we write down the consistency budget.
- Model: commands, aggregates and the events worth recording, with names taken from the business rather than from table columns.
- Build: handlers, the write path, projectors and the read queries, plus the idempotency keys on anything that charges money or sends a message.
- Prove it: tests for the rules, a timed rebuild of each projection, and a retry test that shows a duplicate command does not duplicate the effect.
- Support: dashboards for projector lag and failed commands, and a documented way to add the next command without inventing a new pattern.
Why CodeWiz
- We ship the code: handlers, projectors and migrations land in your Laravel or Node.js repository, not in a slide that names the pattern.
- The smallest split that works: we will not sell an event-sourced platform for a problem that separate query classes would solve.
- Open-source first: PostgreSQL, Redis, Typesense and, when a log is justified, EventStoreDB. No per-event licence as the price of admission.
- Full-stack: the screen, the API and the write model are designed together, so the read model matches what the interface actually renders.
- Fair pricing: premium development without premium prices, from our team in Banja Luka, working remotely with clients locally and internationally. More about CodeWiz.
Frequently Asked Questions
How much does a CQRS implementation cost?
Splitting handlers inside one application, with a couple of dedicated read queries, is a modest project. An event store, several projections and a rebuild path for a live product is a larger one, mostly because of migration and the time spent proving replay. We estimate after we have seen the write rules and the screens that hurt.
Do I need event sourcing to use CQRS?
No. Event sourcing is one way to implement the write side, useful when the history is a requirement. Many CQRS codebases keep a normal relational write model and only separate the queries. We start there unless audit or multiple independent consumers give event sourcing a job.
Will my users see stale data?
Only if the read model is updated asynchronously, and only for as long as the projector takes. We set that budget explicitly, show the user the record they just changed from the write side, and alert on lag. A logical split in one database has no extra lag at all.
Can you add CQRS to an existing Laravel or Node.js application?
Yes. We usually start with one command and one painful query, behind the current API, and leave the rest of the CRUD in place. The pattern spreads as more rules and more screens earn it. A rewrite into a new service is rarely the first step.
Is CQRS only for microservices?
No. The split is about models, not about deployables. It fits a modular monolith particularly well: commands and queries are types in one codebase, and you avoid distributed transactions. Separate services come later, if team ownership or load requires them.
Ready to Separate the Rules from the Screens?
Tell us which actions carry the real rules and which screens are bending the database out of shape. We will propose a level of split that matches the product, not the pattern’s reputation. Contact us to talk it through, or see our CQRS implementation services.