Knowledge Guide
HomeSystem DesignMicroservices Patterns

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

When NOT to use it (the more important list)

The honest default is one model until it hurts. CQRS is a targeted response to a specific asymmetry, not a baseline architecture.

diagram
diagram

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.

ApproachStoresConsistency of readsWhat it buysReach for it when
Single-model CRUDOneStrongThe simplest thing that works; read shape = write shapeDefault. No load asymmetry, simple invariants.
Read replicaOne schema, replicatedEventual (replica lag)Read scaling and read/write isolation with zero model duplicationYou need read throughput but the read shape is fine as-is.
CQRS-lite (shared store, separate query objects)OneStrongSeparated command/query code and tailored read shapes (SQL views/DTOs) — no second store, no async syncThe read shape differs but you do not need independent scaling.
Full CQRS (this lesson)TwoEventualIndependent scaling and tailored shapes and an event streamYou 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:

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.

diagram
diagram

Sources & further reading

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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes