CQRS in systems analyst interviews

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

What CQRS actually means

Command Query Responsibility Segregation is the pattern Greg Young popularised around 2010, building on Bertrand Meyer's older Command-Query Separation principle. The promise sounds modest: split the system into one path that mutates state and another path that reads it. The consequences — separate models, separate stores, eventual consistency between them — are anything but modest.

A systems analyst interviewer at Stripe, Uber, or DoorDash rarely wants you to recite the definition. They want to hear you reason about trade-offs: when does splitting the read and write models reduce coupling, and when does it just double the schemas you have to maintain? The strongest answers tie CQRS to a concrete scenario — a checkout service whose read traffic is 50x the write traffic, an order-history view that needs five denormalised joins, an analytics dashboard that cannot wait for OLTP locks.

The pattern is most often misused as "we have two tables now, so we do CQRS". That is not CQRS. CQRS requires a deliberate division of responsibility at the model level — different objects, different invariants, often different databases. If your write side and read side are still the same ORM entity, you have aliasing, not segregation.

Command vs query — the strict split

The contract is short. A command mutates state and returns nothing useful — at most an acknowledgement or a generated ID. A query returns data and must not change state. No method on a CQRS service is allowed to do both.

PlaceOrder(items, customer_id) → ack
CancelOrder(order_id, reason)  → ack
GetOrder(order_id)             → OrderDTO
ListOrders(customer_id, range) → OrderSummary[]

This sounds pedantic until you look at a typical REST API where POST /orders returns the full hydrated order with computed totals, applied discounts, and recommended upsells. That endpoint is doing four things at once — and it is the reason the read view and the write view drift apart over time. Under CQRS, the command returns 201 Created with an order ID, and the client either polls the read side or subscribes to a notification.

Property Command Query
Side effects Yes — mutates state No — pure read
Return value Ack or generated ID only Full DTO or projection
Validation Business invariants enforced Authorisation only
Idempotency key Required for retries Not applicable
Typical store Normalised, write-optimised Denormalised, read-optimised

Load-bearing rule: if a method both mutates state and returns the new state in a single round-trip, you are not doing CQRS — you are doing CRUD with a fancy name.

Separate models in practice

The write model is built for invariants. It is normalised, transactional, and usually expressed as rich domain entities — an Order aggregate that owns OrderItem, Payment, and ShippingAddress, enforcing rules like "cannot ship before payment is captured". Postgres is the boring, correct choice here. The write side is small, narrow, and pessimistic.

The read model is built for the screen. Each user-facing view becomes its own projection, denormalised to the point of zero joins at query time. The customer's order-history page might be served from a customer_order_history table that already contains the order ID, the merchant name, the item count, the total in the customer's display currency, and the latest tracking status. The admin's fraud-review page reads from a different projection. The finance team's revenue dashboard reads from a third.

Write side (Postgres, normalised):
  orders, order_items, payments, shipments, addresses

Read side (multiple stores, denormalised):
  - customer_order_history     → Postgres materialised view
  - admin_fraud_review         → Elasticsearch index
  - merchant_revenue_dashboard → ClickHouse table, daily aggregate
  - mobile_order_card          → Redis hash, sub-millisecond reads

The interview gotcha here is the question "how many read models is too many?". The honest answer is as many as you have distinct read use cases that justify the maintenance cost. A common pattern at scale is one projection per screen. At smaller scale, three or four projections cover the whole product. Inventing a projection per query method is overkill; reusing one projection across five unrelated screens is a sign you have not actually adopted CQRS.

Synchronization and eventual consistency

The two models talk through events. A command succeeds on the write side, the aggregate emits a domain event (OrderPlaced, PaymentCaptured, OrderShipped), and one or more subscribers update the corresponding read projections.

1. Customer hits POST /orders
2. OrderService.PlaceOrder(...) validates and saves to write store
3. OrderPlaced event published to Kafka / SNS / outbox
4. Projection workers consume the event:
   - customer_order_history projector updates the customer's row
   - merchant_dashboard projector increments the daily aggregate
   - notification_service triggers email + push
5. Read endpoints serve the projections — typically <1s after the write

The price of this architecture is eventual consistency. The gap between "command acknowledged" and "read projection updated" is usually 50 ms to a few seconds, depending on the transport and the projector's batch size. For an order-confirmation page, that gap is invisible. For a financial trade confirmation, it is unacceptable — which is why pure CQRS is rarely used inside trading engines.

The cleanest production pattern is the outbox: the command writes both the aggregate change and the event row in the same transaction, and a separate publisher streams the outbox to the message bus. This avoids the classic dual-write bug where you commit to the database but fail to publish, leaving the read side permanently stale.

Sync strategy Latency Failure mode When to use
Synchronous projection in same transaction ~0 ms Tight coupling, slow writes Tiny scale only
Outbox + change data capture (Debezium) 50-500 ms Best general default Production microservices
Async event bus (Kafka, SNS) 100 ms-2 s Out-of-order events possible Multi-consumer fanout
Periodic rebuild from write store minutes-hours Stale reads between rebuilds Analytics-only projections
Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

When CQRS pays off and when it does not

The pattern fits when reads dominate writes by a wide margin — read-to-write ratios of 20:1 or higher — and when those reads serve genuinely different shapes of data. A marketplace where buyers browse listings constantly while sellers update them occasionally is a textbook CQRS case. So is any system with a heavy analytics workload sitting on top of an OLTP write path.

It also fits when the domain is complex enough that the write model deserves its own object graph. CQRS pairs naturally with Domain-Driven Design and event sourcing — the three together form the "tactical DDD trio" that interviewers at Linear, Notion, and Vercel will probe if you mention any of them. You should be able to explain how the trio reinforces itself: DDD gives you the aggregates, event sourcing gives you the audit log of state changes, and CQRS gives you the read projections that consume those events.

CQRS is overkill for plain CRUD — a settings page, an admin form, a checkout that just persists what the user typed. It is overkill when your team has fewer than three engineers on the service and no on-call rotation, because debugging eventual-consistency bugs at 2am is genuinely harder than debugging a single SQL query. It is overkill when there are no measured performance problems with a unified model; speculative complexity costs more than it saves.

Sanity check before suggesting CQRS in an interview: can you name the specific read use case that needs a different shape than the write model? If not, you are pattern-matching, not designing.

Common pitfalls

The most expensive pitfall is inventing CQRS without inventing the read models. Teams split their services into a "command service" and a "query service", but both query the same normalised tables through the same ORM. Nothing has been segregated. The fix is to commit to at least one denormalised projection per service — if you cannot justify even one, you do not have a CQRS use case.

A close second is ignoring the consistency budget. Once read and write are separate, the product team will eventually ask "why does the order disappear from the history page for two seconds after checkout?" If you have no answer, the next decision is to fall back to synchronous projection updates, which kills the throughput gain that motivated CQRS in the first place. The fix is to acknowledge eventual consistency in the UI from day one — show a "processing" state, use optimistic UI updates, or serve the just-written record from a write-through cache for the first few seconds.

Another trap is letting the read model drift from the write model schema in undocumented ways. Six months in, a field exists on the read projection that nobody can trace back to a write event, because somebody patched it manually during an incident. The fix is to treat projections as fully derived — they can always be dropped and rebuilt from the event log, and you should rebuild them in staging quarterly to prove this still works.

Out-of-order event delivery ruins naive projectors. If OrderShipped arrives before OrderPlaced because of a partition rebalance, the projector either crashes or, worse, writes a row that is silently wrong. The fix is to use sequence numbers per aggregate ID and let the projector hold or replay events until the sequence is contiguous. Kafka partitioning by aggregate ID handles most cases; the rest needs explicit reordering logic.

Finally, teams forget that CQRS does not eliminate transactions on the write side. The aggregate still needs ACID guarantees during command processing — placing an order must atomically reserve inventory, charge payment, and emit the event. Splitting commands and queries does not let you avoid distributed-transaction questions; if anything, the interviewer will push harder on how you handle the write-side atomicity, because that is now the only place strong consistency lives.

Want to drill systems-analyst architecture questions like this every day? NAILDD ships 500+ interview problems across CQRS, sagas, CAP, and the rest of the distributed-systems canon.

FAQ

Is CQRS the same thing as event sourcing?

No. They are independent patterns that happen to compose well. CQRS is about splitting reads from writes; event sourcing is about persisting state as an append-only log of events. You can do CQRS with two normal databases and zero events, and you can do event sourcing with a unified read-and-write model. The combination is popular because the event log gives you a natural way to build and rebuild read projections, but interviewers will catch you out if you conflate the two.

How do you handle a read-your-own-write scenario?

The classic example is a user posting a comment and immediately refreshing the page. With pure eventual consistency, the comment may not be visible for several hundred milliseconds. The fix is to serve the just-written record from a session-scoped cache or the write store itself for that one user for a short window — most teams write the new record to both stores synchronously for the requesting client and rely on the async projector for everyone else. This is sometimes called a "self-read consistency" or "session consistency" guarantee.

Can CQRS work with a single database?

Yes, and this is a sensible starting point. Use one Postgres instance with the normalised write tables and materialised views or denormalised read tables, refreshed by triggers or background workers. You get most of the model-separation benefits without operating a message bus. The downside is that you cannot scale reads and writes independently, so once you outgrow a single instance, you migrate the read side to a separate store. Calling this "CQRS lite" in an interview signals pragmatism.

What is the relationship between CQRS and microservices?

CQRS is orthogonal to service boundaries — you can apply it inside a single monolith or across a fleet of microservices. It does, however, fit the microservice mental model well: each service owns its write model and exposes its read projections to others through events. Be careful in interviews not to imply that "going microservices" automatically means adopting CQRS. The two decisions should be made separately, with separate justifications.

How do you test a CQRS system?

Test the command side with classical unit and integration tests around the aggregate invariants — given a state and a command, assert the resulting events. Test each projector independently by feeding it a known event stream and asserting the projection's final state. End-to-end tests then verify that a command followed by a poll on the read side eventually returns the expected projection. The "eventually" is the hard part — most teams use a bounded retry with a timeout, typically 5 to 10 seconds, to avoid flakiness without hiding real bugs.

When should I bring up CQRS unprompted in a systems-analyst interview?

When the prompt mentions any of: heavy read-versus-write asymmetry, multiple distinct user-facing views of the same underlying data, an analytics requirement layered on top of an OLTP system, or an audit-log requirement that hints at event sourcing. Avoid bringing it up for prompts about simple CRUD apps, internal admin tools, or anything where the interviewer has emphasised a small team and a tight deadline. Suggesting CQRS for a two-week MVP is a credibility leak.