Separate the model used to write data from the model used to read it, so each can be optimized independently.
A single data model has to serve both writes (validate an order, enforce business rules, keep invariants) and reads (render a dashboard joining five tables, paginate a feed). Optimizing the schema for one usually makes the other slower or more awkward — normalized for safe writes fights against denormalized for fast reads.
Commands (writes) go through a model focused on validation and consistency. Queries (reads) go through a separate model — often a denormalized, pre-joined view — built specifically for how the data is displayed. The read model is updated asynchronously after each write, so it's typically eventually consistent.
`placeOrder` only touches the write model and enforces the rule. The read model is a separately shaped projection, rebuilt from write events.
// Write side: enforces invariants, source of truth
const writeModel = { orders: new Map() };
function placeOrder(id, items) {
if (items.length === 0) throw new Error("order must have at least one item");
writeModel.orders.set(id, { id, items, status: "placed" });
projectToReadModel(id); // keep the read side up to date
}
// Read side: denormalized, shaped for a dashboard
const readModel = { orderSummaries: new Map() };
function projectToReadModel(id) {
const order = writeModel.orders.get(id);
readModel.orderSummaries.set(id, {
id: order.id,
itemCount: order.items.length,
status: order.status,
});
}
function getOrderSummary(id) {
return readModel.orderSummaries.get(id); // fast, pre-shaped for display
}
placeOrder("order-1", ["book", "pen"]);
console.log(getOrderSummary("order-1")); // { id: 'order-1', itemCount: 2, status: 'placed' }