CQRS Pattern An Example
What CQRS actually is
CQRS — Command Query Responsibility Segregation — splits a system into two models. A write model handles commands (state-changing intents like "create user" or "update email") and a read model answers queries. Instead of one set of classes and one table serving both jobs, each side gets its own shape, its own store, and its own code path: the write side stays normalized and enforces invariants; the read side is denormalized and shaped exactly for the screens that consume it. The two stores are kept in sync by a stream of events.
When to reach for CQRS
- Reads and writes have very different loads. A catalogue read millions of times but written a few hundred times a day lets you scale a denormalized read store independently of the transactional write store.
- The read shape does not match the write shape. Dashboards that join across many aggregates are painful on a normalized write schema; a purpose-built projection removes the joins.
- Rich write-side invariants. Task-based UIs, event sourcing, and collaborative editing benefit from a command model that is not polluted by query concerns.
- You already need events for integration, audit, or replay — CQRS falls out naturally once commands emit events.
When NOT to use it (the more important list)
- Plain CRUD. If a screen edits the same fields it displays and there are no interesting invariants, a single model over a single table is simpler, faster to build, and easier to reason about. Splitting it buys you two models, a sync mechanism, and a consistency window — and gains nothing.
- Small or uniform load. If reads and writes are comparable and modest, independent scaling solves a problem you do not have.
- An unstable or unfamiliar domain. CQRS multiplies the moving parts; on a model that is still churning, that overhead is expensive. Start with one model and extract CQRS only where a measured pain appears.
The honest default is one model until it hurts. CQRS is a targeted response to a specific asymmetry, not a baseline architecture.
CQRS vs. the alternatives
Before adopting full CQRS, weigh it against three cheaper points on the same spectrum. Each solves a subset of the same problems for a fraction of the cost.
| Approach | Stores | Consistency of reads | What it buys | Reach for it when |
|---|---|---|---|---|
| Single-model CRUD | One | Strong | The simplest thing that works; read shape = write shape | Default. No load asymmetry, simple invariants. |
| Read replica | One schema, replicated | Eventual (replica lag) | Read scaling and read/write isolation with zero model duplication | You need read throughput but the read shape is fine as-is. |
| CQRS-lite (shared store, separate query objects) | One | Strong | Separated command/query code and tailored read shapes (SQL views/DTOs) — no second store, no async sync | The read shape differs but you do not need independent scaling. |
| Full CQRS (this lesson) | Two | Eventual | Independent scaling and tailored shapes and an event stream | You genuinely need all three. |
Notice the progression: each step adds capability and adds cost. The moment you cross into separate stores, you also cross into eventual consistency — the trade-off the rest of this lesson makes concrete. If you only want tailored read shapes, CQRS-lite gives you that while keeping reads strongly consistent; if you only want read throughput, a read replica is far cheaper than a projection pipeline.
The write side
Commands are state-changing intents. A handler validates them, mutates the normalized write store, and emits an event describing what happened. The write store is the source of truth; it never serves queries.
interface Command {}
interface Event {}
interface Query<R> {}
class CreateUserCommand implements Command {
final String userId, name, email;
CreateUserCommand(String userId, String name, String email) {
this.userId = userId; this.name = name; this.email = email;
}
}
class UpdateUserEmailCommand implements Command {
final String userId, newEmail;
UpdateUserEmailCommand(String userId, String newEmail) {
this.userId = userId; this.newEmail = newEmail;
}
}
// Write model — normalized, guards invariants, the source of truth.
class User {
final String id; String name, email;
User(String id, String name, String email) { this.id=id; this.name=name; this.email=email; }
}
class WriteStore {
private final Map<String,User> users = new HashMap<>();
void save(User u) { users.put(u.id, u); }
User findById(String id) { return users.get(id); }
}
class CreateUserCommandHandler implements CommandHandler<CreateUserCommand> {
private final WriteStore writeStore; private final EventStore eventStore;
CreateUserCommandHandler(WriteStore w, EventStore e) { writeStore=w; eventStore=e; }
public void handle(CreateUserCommand c) {
writeStore.save(new User(c.userId, c.name, c.email));
eventStore.append(new UserCreatedEvent(c.userId, c.name, c.email));
}
}
class UpdateUserEmailCommandHandler implements CommandHandler<UpdateUserEmailCommand> {
private final WriteStore writeStore; private final EventStore eventStore;
UpdateUserEmailCommandHandler(WriteStore w, EventStore e) { writeStore=w; eventStore=e; }
public void handle(UpdateUserEmailCommand c) {
if (c.newEmail == null || !c.newEmail.contains("@"))
throw new IllegalArgumentException("Invalid email format");
User u = writeStore.findById(c.userId);
if (u != null) {
u.email = c.newEmail;
writeStore.save(u);
eventStore.append(new UserEmailUpdatedEvent(c.userId, c.newEmail));
}
}
}The read side
The read model is a separate store holding denormalized rows shaped for the screen. A projector consumes events and maintains those rows — this is the only thing that keeps the two stores in sync. Query handlers read only from the read store; they never touch the write store.
// Events — past-tense facts emitted by the write side.
class UserCreatedEvent implements Event {
final String userId, name, email;
UserCreatedEvent(String userId, String name, String email) {
this.userId=userId; this.name=name; this.email=email; }
}
class UserEmailUpdatedEvent implements Event {
final String userId, newEmail;
UserEmailUpdatedEvent(String userId, String newEmail) {
this.userId=userId; this.newEmail=newEmail; }
}
class GetUserProfileQuery implements Query<UserProfileView> {
final String userId; GetUserProfileQuery(String userId){ this.userId=userId; }
}
class ReadUserEmailQuery implements Query<String> {
final String userId; ReadUserEmailQuery(String userId){ this.userId=userId; }
}
// Read model — denormalized, shaped for the screen, a SEPARATE store.
class UserProfileView {
final String id; String name, email;
UserProfileView(String id, String name, String email){ this.id=id; this.name=name; this.email=email; }
public String toString(){ return "UserProfileView{id="+id+", name="+name+", email="+email+"}"; }
}
class ReadStore {
private final Map<String,UserProfileView> views = new HashMap<>();
void put(UserProfileView v){ views.put(v.id, v); }
UserProfileView get(String id){ return views.get(id); }
}
// The projector converts events into read rows — the ONLY thing that syncs the stores.
class UserProjector {
private final ReadStore readStore;
UserProjector(ReadStore r){ readStore = r; }
void on(Event e) {
if (e instanceof UserCreatedEvent) {
UserCreatedEvent ev = (UserCreatedEvent) e;
readStore.put(new UserProfileView(ev.userId, ev.name, ev.email));
} else if (e instanceof UserEmailUpdatedEvent) {
UserEmailUpdatedEvent ev = (UserEmailUpdatedEvent) e;
UserProfileView v = readStore.get(ev.userId);
if (v != null) v.email = ev.newEmail;
}
}
}
// Query handlers read ONLY from the read store.
class GetUserProfileQueryHandler implements QueryHandler<GetUserProfileQuery, UserProfileView> {
private final ReadStore readStore;
GetUserProfileQueryHandler(ReadStore r){ readStore = r; }
public UserProfileView handle(GetUserProfileQuery q){ return readStore.get(q.userId); }
}
class ReadUserEmailQueryHandler implements QueryHandler<ReadUserEmailQuery, String> {
private final ReadStore readStore;
ReadUserEmailQueryHandler(ReadStore r){ readStore = r; }
public String handle(ReadUserEmailQuery q){
UserProfileView v = readStore.get(q.userId);
return v != null ? v.email : null;
}
}Event store and the buses
The event store is the seam between the two sides. Read the comment on append() carefully: here it notifies subscribers synchronously, in the same thread, before returning. That is a deliberate simplification for a runnable demo — the consistency section below explains why production does the opposite.
Both buses share one shape: a registry keyed by message type, so what you register and what you dispatch are always the same type. (An earlier version of this example wired a bus to a single concrete handler and then dispatched a different command type through it — a mismatch this generic registry makes impossible.)
// Event store. SIMPLIFICATION: append() fans out to subscribers synchronously,
// in THIS thread, before returning. Production would do this asynchronously.
class EventStore {
private final List<Event> log = new ArrayList<>();
private final List<Consumer<Event>> subscribers = new ArrayList<>();
void subscribe(Consumer<Event> s){ subscribers.add(s); }
void append(Event e) {
log.add(e);
for (Consumer<Event> s : subscribers) s.accept(e); // synchronous fan-out
}
}
interface CommandHandler<C extends Command> { void handle(C command); }
interface QueryHandler<Q extends Query<R>, R> { R handle(Q query); }
class CommandBus {
private final Map<Class<?>,CommandHandler<?>> handlers = new HashMap<>();
<C extends Command> void register(Class<C> t, CommandHandler<C> h){ handlers.put(t, h); }
@SuppressWarnings("unchecked")
<C extends Command> void dispatch(C command) {
CommandHandler<C> h = (CommandHandler<C>) handlers.get(command.getClass());
if (h == null) throw new IllegalStateException("No handler for " + command.getClass());
h.handle(command);
}
}
class QueryBus {
private final Map<Class<?>,QueryHandler<?,?>> handlers = new HashMap<>();
<Q extends Query<R>, R> void register(Class<Q> t, QueryHandler<Q,R> h){ handlers.put(t, h); }
@SuppressWarnings("unchecked")
<Q extends Query<R>, R> R dispatch(Q query) {
QueryHandler<Q,R> h = (QueryHandler<Q,R>) handlers.get(query.getClass());
if (h == null) throw new IllegalStateException("No handler for " + query.getClass());
return h.handle(query);
}
}Putting it all together
Wire the projector to the event stream, register handlers on each bus, then send two commands and two queries. The reads deliberately hit the read store — the projection — never the write store.
import java.util.*;
import java.util.function.Consumer;
public class CQRSApplication {
public static void main(String[] args) {
WriteStore writeStore = new WriteStore();
ReadStore readStore = new ReadStore();
EventStore eventStore = new EventStore();
// Wire the projector to the event stream — this is the sync mechanism.
UserProjector projector = new UserProjector(readStore);
eventStore.subscribe(projector::on);
CommandBus commandBus = new CommandBus();
commandBus.register(CreateUserCommand.class, new CreateUserCommandHandler(writeStore, eventStore));
commandBus.register(UpdateUserEmailCommand.class, new UpdateUserEmailCommandHandler(writeStore, eventStore));
QueryBus queryBus = new QueryBus();
queryBus.register(GetUserProfileQuery.class, new GetUserProfileQueryHandler(readStore));
queryBus.register(ReadUserEmailQuery.class, new ReadUserEmailQueryHandler(readStore));
commandBus.dispatch(new CreateUserCommand("1", "Alice", "alice@example.com"));
commandBus.dispatch(new UpdateUserEmailCommand("1", "alice_new@example.com"));
// Reads hit the READ store (the projection) — never the write store.
String email = queryBus.dispatch(new ReadUserEmailQuery("1"));
System.out.println("Updated Email: " + email);
UserProfileView profile = queryBus.dispatch(new GetUserProfileQuery("1"));
System.out.println(profile);
}
}Compiled with javac 1.8 and run, it prints exactly:
Updated Email: alice_new@example.com
UserProfileView{id=1, name=Alice, email=alice_new@example.com}The trade-off this demo hides: eventual consistency
The read came back fully up to date — alice_new@example.com. That is a property of this demo, not of CQRS. Look again at EventStore.append: it calls every subscriber synchronously, on the same thread, before it returns. So by the time commandBus.dispatch(...) finishes, the projector has already run and the read store is updated. Reads and writes appear to move in lockstep only because everything happens in one process, one thread, one heap.
Real separate-store CQRS does not work this way. The projector consumes events asynchronously — off a durable log or message queue, usually in a different process on a different machine, writing to a different database. Between the moment a command commits and the moment the projection catches up there is a window in which the read store is stale. That is eventual consistency, and it is the defining cost of splitting the stores.
Concretely: if Alice updates her email and immediately reloads, she may see the old address for milliseconds — or seconds, if the projector is backed up. You must design for that window:
- Read-your-writes: route the just-writing user's next read to the write model (or a version-tagged read) so they never see stale data, even if others briefly do.
- Optimistic UI: show the value the user just submitted locally, without waiting for the projection.
- Version / sequence tokens: the command returns a version; the read side waits until its projection has caught up to that version before answering.
- Just tolerate it: for a catalogue or analytics dashboard, a second of lag is fine — which is exactly why those are good CQRS candidates.
To make this demo behave like production, replace the synchronous for-loop in append with a queue that the projector drains on its own thread. The instant you do, a query issued in the gap can observe a stale read. The lesson keeps it synchronous only so the output is deterministic; never mistake that for how the pattern behaves once the stores are truly separate.
Sources & further reading
- Greg Young — CQRS Documents (2010): the origin of the term and the command/query model split.
- Martin Fowler — CQRS (martinfowler.com bliki): when the pattern helps, and its risks — including the caution that it should be applied only to specific portions of a system.
- Microsoft — CQRS pattern, Azure Architecture Center: read-store/projection design and the eventual-consistency guidance for separate stores.
The worked example above was reconstructed and verified: all classes compile on javac 1.8 and produce exactly the output shown.
🤖 Don't fully get this? Learn it with Claude
Stuck on CQRS Pattern An Example? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **CQRS Pattern An Example** (System Design) and want to truly understand it. Explain CQRS Pattern An Example from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **CQRS Pattern An Example** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **CQRS Pattern An Example** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **CQRS Pattern An Example** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.