CQRS and read models

Microservices · lesson 21 of 33 · 4 min read

Split the write shape from the read shape when one table cannot serve both well.

Open this lesson in the learning hub

Key points

  • CQRS is one idea: the model you write through does not have to be the model you read from.
  • Writes stay normalised and validated. Reads get a flat table shaped exactly like the screen that asks for them.
  • A read model is built by folding events, so it is disposable: drop it, replay the log, and it comes back identical.
  • It is eventually consistent by construction. Right after a write, read back from the write side or the user misses their own change.
  • Do not reach for it by default. Use it where read and write loads genuinely differ, never on ordinary CRUD.

Example

import java.util.List;

/** The write side is a log of facts. The read side is a fold of that log. */
public class Main {

    record Event(String orderId, String type, long cents) {}

    // One flat row, shaped for the dashboard rather than for correctness.
    static final class Summary {
        int liveOrders;
        long totalCents;
    }

    public static void main(String[] args) {
        List<Event> log = List.of(
                new Event("o-1", "OrderPlaced", 4999),
                new Event("o-2", "OrderPlaced", 1500),
                new Event("o-1", "OrderCancelled", 4999),
                new Event("o-3", "OrderPlaced", 2500));

        Summary view = project(log);
        System.out.println("live orders : " + view.liveOrders);
        System.out.println("total cents : " + view.totalCents);

        // Read models are disposable: throw it away and rebuild from the log.
        Summary rebuilt = project(log);
        System.out.println("rebuild matches: "
                + (rebuilt.totalCents == view.totalCents
                   && rebuilt.liveOrders == view.liveOrders));
    }

    static Summary project(List<Event> log) {
        Summary s = new Summary();
        for (Event e : log) {
            switch (e.type()) {
                case "OrderPlaced"    -> { s.liveOrders++; s.totalCents += e.cents(); }
                case "OrderCancelled" -> { s.liveOrders--; s.totalCents -= e.cents(); }
                default -> { }
            }
        }
        return s;
    }
}

Shape the read side around the question being asked, and rebuild it from the log.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Microservices course, and every lesson in it is listed on the Microservices contents page.