A checkout flow writes to normalized tables because transactional integrity demands it. The product listing page reads those same tables thousands of times a minute and pays for every join. Each side eventually drags the other down.

Command Query Responsibility Segregation (CQRS) pulls the two apart, giving writes and reads a model each, and a standing counterargument says most applications shouldn't bother. Both positions hold up, so the useful question is which one describes your system.

This guide covers what CQRS specifies, the workloads that justify it, the far more common case for skipping it, and how the read/write split maps onto serverless infrastructure.

**Key takeaways:**

- CQRS uses a different model to update data than the model used to read it, so each side takes the form its access pattern needs.

- The pattern earns its complexity when reads outnumber writes by an order of magnitude and the two access patterns have diverged in form rather than only in volume.

- Most applications should skip CQRS, because a unified model with an index or a cache solves the same throughput problem without dual-model maintenance.

- CQRS and event sourcing are independent patterns, and adopting both at once compounds implementation complexity that neither requires on its own.

- Separate read and write models mean eventual consistency, a user-facing cost rather than an implementation detail.

## [Copy link to heading](#what-is-cqrs)What is CQRS?

CQRS uses one model to change data and separate models to read it. The write model enforces validation, domain rules, and transactional integrity. Read models serve precomputed projections and carry no business logic.

The rule underneath it is older. Command-query separation (CQS), which Bertrand Meyer introduced in object-oriented design, divides methods into queries that return a result without changing state and commands that change state without returning a value. CQRS lifts that rule from method signatures to architecture, so the write model and the read models become separate objects free to diverge.

## [Copy link to heading](#the-read/write-problem-cqrs-solves)The read/write problem CQRS solves

Two workloads sharing one model is not inherently a problem. It becomes one when those workloads diverge along both axes at once, in how much traffic each receives and in what form the data needs to take.

### [Copy link to heading](#scale-divergence-and-form-divergence)Scale divergence and form divergence

Scale divergence is the familiar case, where reads outnumber writes by 10:1 or more and the database tuned for write integrity spends most of its capacity serving queries it was never built for. Form divergence is the case teams recognize later, when the schema that keeps writes correct is the same schema making reads slow.

The two models want opposite things on every axis that matters:

| Dimension | Write model | Read model |
| --- | --- | --- |
| Schema form | Normalized, constraint-enforced | Denormalized, query-shaped |
| Tuned for | Transactional integrity | Retrieval latency |
| Domain logic | Validation and business invariants | None |
| Typical scaling need | Vertical, fewer instances | Horizontal, absorbs query volume |

One model has to pick a side on each of those dimensions. Whichever side it picks, the other workload pays for it, and the bill grows as the domain does.

### [Copy link to heading](#benefits-of-read/write-separation)Benefits of read/write separation

Once the models are separate, a change that helps reads can no longer hurt writes.

Splitting them buys four things:

- **Independent scaling:** Read capacity grows without touching the write path, so a spike on the listing page never competes with checkout for database connections.

- **Query-shaped schemas:** A read model stores data in the form the page asks for, so a view that would have joined six tables reads one row.

- **Per-operation consistency:** Each read model sets its own staleness tolerance, so a search index can rebuild on a schedule while an account balance reads through to the source.

- **Intent-carrying commands:** Commands carry intent rather than field updates, so a rule spanning several entities lives in one place instead of being re-checked at every call site.

Every one of those benefits depends on the read models being current. Keeping them current is the work CQRS adds, and most of the pattern's operational difficulty lives there.

## [Copy link to heading](#how-the-cqrs-pattern-works)How the CQRS pattern works

Implementations vary widely in weight, from a code-level split inside one database to separate data stores connected by an event pipeline. Three moving parts stay the same across all of them.

### [Copy link to heading](#the-command-model)The command model

A command model receives intent rather than data. Each command names something the domain should do, such as submitting an order or cancelling a subscription, and the model validates that request against business invariants before committing anything.

Transactional integrity lives here. The command model owns the normalized schema, enforces constraints, and either commits the full operation or rejects it. Invariants that span several entities get one place to be enforced, which is why multi-step workflows benefit.

The design mistake worth avoiding is letting commands return data. Once a command starts returning the object it created, the read path quietly begins depending on write-path forms, eroding the separation from the inside.

### [Copy link to heading](#the-query-model)The query model

A query model serves projections. It runs no validation, applies no business rules, and produces no side effects. A projection is a precomputed view of state shaped for one specific consumer, which is why one write model can feed several read models.

Keeping domain logic out is deliberate. Query handlers that start making decisions turn into a second write model without transactional guarantees, the worst of both structures.

### [Copy link to heading](#the-projection-bridge)The projection bridge

Something has to tell the read models when the write model commits. In the lightest implementations, both sides share one database and the bridge is a transaction boundary, so reads see writes immediately.

Heavier implementations have the write side emit an event, and a projection worker updates a separate read store asynchronously. The [implementation spectrum](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/cqrs-pattern.html) runs from shared relational infrastructure to distinct read and write technologies, and that choice determines almost everything else about the system.

Shared-database CQRS keeps strong consistency and gives up independent scaling. Separate-store CQRS buys independent scaling and accepts eventual consistency in exchange.

## [Copy link to heading](#when-you-don't-need-cqrs)When you don't need CQRS

Most applications should not adopt CQRS. Splitting the models only helps when read and write access patterns have already diverged, and four situations mean they haven't.

### [Copy link to heading](#your-domain-is-mostly-crud)Your domain is mostly CRUD

Plenty of applications are create, read, update, delete (CRUD) and nothing more. A settings page, an admin panel, or an internal directory stores what a form submitted and returns it on request, with business rules that amount to field validation.

Splitting a CRUD domain into command and query models produces two representations of the same data, adding indirection with no invariant worth enforcing between them.

### [Copy link to heading](#your-bottleneck-is-volume,-not-form)Your bottleneck is volume, not form

Slow reads usually mean a missing index, an N+1 query, or a query that no cache sits in front of. All three have direct fixes, and none requires a second model.

If more hardware would fix the query, the problem is volume, and CQRS addresses form.

### [Copy link to heading](#your-team-can't-operate-two-models)Your team can't operate two models

Two models means two deployment paths, two schema migrations to keep aligned, and a projection pipeline that needs monitoring and a rebuild story. A write-side schema change that isn't mirrored in projection logic produces data corruption that no exception surfaces, because both models are individually valid and only disagree with each other.

Developers who haven't run an eventually consistent system before absorb that learning curve during an incident. Rapidly changing prototypes and teams without the operational headroom for two models are both better served by one.

### [Copy link to heading](#you'd-apply-it-system-wide)You'd apply it system-wide

Applying CQRS across an entire system because one bounded context justifies it is a common and costly adoption mistake. The complexity multiplies across every context, and the benefit stays confined to the one that needed it.

Order processing might warrant an explicit command model with real invariants. The user preferences table in the same application almost certainly does not. Bounded context is the right unit of adoption, and CRUD-dominant contexts are better left on a single model.

Three lighter techniques cover much of the same ground before a separate read store becomes necessary:

| Technique | Consistency guarantee | Read-your-own-writes | Operational cost |
| --- | --- | --- | --- |
| CDN cache with stale-while-revalidate | Bounded staleness, controlled per tag | Available in Server Actions via `updateTag` | Lowest, framework-managed |
| [Database read replicas](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PostgreSQL.Replication.ReadReplicas.html) | Asynchronous replication lag | Requires sticky routing to the primary | Low to medium |
| Materialized views in the same database | Stale until the next refresh, with [no incremental refresh](https://wiki.postgresql.org/wiki/Incremental_View_Maintenance) in stock PostgreSQL | No | Medium |
| Full CQRS with a separate read store | Eventual, with projection lag between commit and read | Requires explicit mitigation | Highest |

Read replicas absorb query volume while preserving the primary's schema, so slow query forms stay slow. Matching the technique to the problem comes before paying for a separate read store.

## [Copy link to heading](#cqrs-and-event-sourcing)CQRS and event sourcing

CQRS and event sourcing appear together in enough write-ups that many engineers treat them as one pattern. One decides which model serves reads and which handles writes, the other decides how state gets stored underneath, and nothing forces a team to adopt both.

### [Copy link to heading](#why-event-sourcing-feeds-cqrs)Why event sourcing feeds CQRS

Event sourcing stores state changes as an append-only log of events and treats that log as the authoritative record. The log is already a stream of everything the write model did, so a projection process can consume it directly.

CQRS needs a signal when the write model commits. Event sourcing produces that signal as a byproduct of how it stores data, so the two patterns slot together with almost no adapter code. Broader [event-driven architecture](https://vercel.com/i/event-driven-architecture) builds on the same relationship.

### [Copy link to heading](#why-you-can-adopt-either-one-alone)Why you can adopt either one alone

Running CQRS against a conventional relational database works without any event log. The write model commits a transaction, the application publishes a message or invalidates a cache tag, and the projection updates.

Event sourcing without CQRS also works, though it is rarer. Applications that need an audit trail or temporal queries sometimes store events and read from a single projection.

Adopting both together compounds the implementation work, since you take on event-store semantics, replay handling, and projection versioning at the same time as the read/write split. Developers who run CQRS against a conventional database avoid that entire layer.

## [Copy link to heading](#what-eventual-consistency-costs-cqrs-users)What eventual consistency costs CQRS users

Once the read models update asynchronously, eventual consistency stops being an implementation detail and becomes a property the whole application inherits. Every workflow above it has to account for the gap between a write committing and the corresponding read reflecting it.

### [Copy link to heading](#the-read-your-own-writes-failure)The read-your-own-writes failure

The failure mode is specific. Imagine a user submits a form. The write commits, the redirect lands on a page reading a projection that hasn't caught up, and the user concludes the operation failed.

They then retry. Now you have a duplicate order, a duplicate comment, or a support ticket, none of which the consistency model caused directly and all of which it made likely.

Read-your-own-writes has to be designed in deliberately, per operation, because it doesn't come for free once the models are separate. The usual mitigations are routing the immediate post-write read to the write model, or blocking on projection acknowledgment for that one request.

### [Copy link to heading](#when-lag-stretches-beyond-its-normal-window)When lag stretches beyond its normal window

Eventual consistency behaves well in steady state and stretches under the conditions that make it hardest to diagnose. A deployment, a queue rebalance, or a projection restart can stretch read-model lag into minutes.

It's worth deciding in advance which workflows tolerate stale reads and which do not. A product listing can be seconds behind without anyone noticing. An inventory count that drives an add-to-cart decision cannot.

## [Copy link to heading](#how-vercel-implements-cqrs-on-serverless)How Vercel implements CQRS on serverless

Serverless changes the economics of the pattern, because separate read and write paths stop requiring separate infrastructure to provision. On Vercel, cached reads serve from the CDN, mutations run through Server Actions, and [Vercel Queues](https://vercel.com/docs/queues) carry events to projection consumers. The queue and workflow primitives work from any framework, while the cache directives are the Next.js expression of the same split.

### [Copy link to heading](#serving-reads-without-invoking-a-function)Serving reads without invoking a function

Read-heavy applications spend most of their compute budget re-deriving responses that haven't changed since the last request. CQRS read models exist to eliminate that cost.

[Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration) (ISR) removes that work at the infrastructure layer. A page whose content is cached prerenders into a static shell, and a cache hit serves from the [CDN](https://vercel.com/docs/cdn) without invoking a function.

The cached HTML and data payloads amount to a materialized projection of the write-side database, updated asynchronously. You get CQRS's read side without a projection pipeline to operate.

### [Copy link to heading](#fanning-one-write-out-to-several-read-models)Fanning one write out to several read models

Bridging the two models is the part teams end up operating themselves, starting with a broker choice between [RabbitMQ and Kafka](https://vercel.com/i/rabbitmq-vs-kafka) and widening into the broader problem of [workflow orchestration](https://vercel.com/i/workflow-orchestration).

[Vercel Queues](https://vercel.com/docs/queues/concepts) provide the same primitive as managed infrastructure. A topic is a durable, append-only log. Each consumer group subscribes independently, tracking its own position and processing at its own pace. Groups are isolated, so a slow or failing consumer in one has no effect on any other.

Isolation makes several read models practical. Multiple route files consuming the same topic create separate consumer groups, each receiving a copy of every message, so a search index and a denormalized listing table update from one published event. Projection rebuilds follow the same mechanism, since a new consumer group in poll mode starts from the beginning of the topic and reads all non-expired history, so backfilling needs no republishing from the write side.

### [Copy link to heading](#designing-projections-around-the-delivery-guarantees)Designing projections around the delivery guarantees

Queues deliver at-least-once, so a projection consumer has to be idempotent. Setting a value rather than incrementing it, or deduplicating on message ID, keeps a redelivered event from double-applying. [Vercel Workflows](https://vercel.com/docs/workflows) handle the harder case, adding durable steps on the same queue infrastructure when a projection spans multiple dependent operations.

Ordering needs the same treatment. Delivery follows approximate write order, without a first-in, first-out (FIFO) guarantee, and retried messages carry lower priority than new ones. Projections depending on strict sequence should carry sequence numbers in the payload and reorder on the consumer side.

There is no built-in dead-letter queue. Poisoned messages are handled through the SDK's `retry` callback. It controls backoff and can acknowledge a message to stop the retries:

```
import { handleCallback } from '@/lib/queue';

export const POST = handleCallback(
  async (message) => {
    await updateProductProjection(message);
  },
  {
    retry: (error, metadata) => {
      if (metadata.deliveryCount > 10) {
        return { acknowledge: true };
      }
      return { afterSeconds: Math.min(300, 2 ** metadata.deliveryCount * 5) };
    },
  },
);
```

Acknowledging a poisoned message stops the retries without stalling the projection, since messages with no delivery attempts are prioritized over retried ones.

### [Copy link to heading](#choosing-read-your-own-writes-per-mutation)Choosing read-your-own-writes per mutation

For projections that live in the cache rather than a separate store, the consistency choice narrows to two functions. Call `revalidateTag` with a cache profile for stale-while-revalidate behavior, where the next request receives the cached response while fresh data loads behind it:

```
import { revalidateTag } from 'next/cache';

revalidateTag('products', 'max');
```

Call [`updateTag`](https://nextjs.org/docs/app/api-reference/functions/updateTag) when the user must see their own change immediately. It expires the entry so the next request waits for fresh data. The function runs only inside [Server Actions](https://nextjs.org/docs/app/guides/server-actions), which is where read-your-own-writes belongs:

```
'use server';

import { updateTag } from 'next/cache';

export async function createProduct(formData: FormData) {
  await db.products.create(formData);
  updateTag('products');
}
```

Both functions run in the same server round trip as the mutation, so the write and the invalidation complete together. As of [Next.js 16](https://nextjs.org/blog/next-16), `revalidateTag` takes a cacheLife profile as its second argument, and the single-argument form is deprecated.

### [Copy link to heading](#proving-the-asymmetry-at-production-volume)Proving the asymmetry at production volume

The economic case for separating the paths shows up in the traffic distribution. Across [Black Friday 2025](/blog/bfcm-2025), more than 56.9 billion requests were served directly from Vercel's global cache, against roughly 1.5 billion ISR writes propagating catalog, pricing, and content updates.

[GitBook](https://vercel.com/customers/how-gitbook-serves-30000-sites-with-sub-second-content-updates) runs the tagged version of that split across a multi-tenant documentation platform, serving 120 million monthly page views across 30,000 sites. Cached data is tagged by content unit, so a merge event invalidates only the tags that changed rather than the tenant's whole site.

GitBook processes 40,000 of those invalidations daily, each resolving in under 300 milliseconds. Tag granularity determines whether that works, since broad purges get expensive at multi-tenant scale.

## [Copy link to heading](#ship-domain-logic-rather-than-rebuilding-the-split)Ship domain logic rather than rebuilding the split

Few applications need full CQRS, and that is the useful result rather than a disappointing one. A model tuned for transactional integrity does punish read throughput, and the reverse holds equally. Most of that separation is infrastructure work, which a platform can absorb.

Understanding CQRS is still worth the effort, because the pressure it responds to is real and a growing data layer will eventually hit it. Reserve the full pattern for the bounded contexts where write complexity outgrows what the platform provides.

Vercel supplies the read/write separation as platform primitives:

- **ISR:** Cached responses serve from the CDN without invoking a function, and concurrent regeneration collapses to a single invocation.

- **Cache tags:** Tag-scoped invalidation bridges write events to the read path with the granularity a multi-tenant system needs.

- **Vercel Queues:** Durable topics fan one published event out to isolated consumer groups, so several projections update from a single write.

- **Vercel Workflows:** Durable steps and run state handle projections that span multiple dependent operations.

- [**Vercel Functions**](https://vercel.com/docs/functions)**:** Write-path compute scales to actual mutation volume rather than being provisioned against read traffic.

[Start a new project](https://vercel.com/new) on Vercel and deploy on your first `git push`, or browse [vercel.com/templates](https://vercel.com/templates) for a starting point with caching and mutations already wired together.

## [Copy link to heading](#frequently-asked-questions-about-cqrs)Frequently asked questions about CQRS

### [Copy link to heading](#what-is-the-difference-between-cqrs-and-crud)What is the difference between CQRS and CRUD?

CRUD uses one model for every operation, so the same entity representation handles creates, reads, updates, and deletes. CQRS splits that into a command model that enforces invariants and one or more read models shaped for queries. CRUD stays the correct default until read and write access patterns diverge structurally.

### [Copy link to heading](#does-cqrs-require-event-sourcing)Does CQRS require event sourcing?

No, and adopting them in sequence usually beats adopting them together. CQRS against a conventional database comes first, and event sourcing follows only if you need an audit trail, temporal queries, or replay. Retrofitting an event log later costs less than debugging both patterns at once.

### [Copy link to heading](#is-cqrs-better-than-read-replicas)Is CQRS better than read replicas?

They solve different problems. Replicas copy the primary's schema, so they add read capacity while leaving slow query forms unchanged. CQRS changes the schema itself, using a projection built for your query patterns. Replicas fit a volume problem, and CQRS fits a form problem.

### [Copy link to heading](#can-you-adopt-cqrs-in-one-part-of-an-application)Can you adopt CQRS in one part of an application?

Yes, and the transactional boundary is where the seam belongs. An invariant should sit inside one context so a command never spans two models. Where a CQRS context meets a CRUD one, the crossing works better as an integration point with its own contract than as a shared table.