Most teams reach for CQRS and event sourcing after a single database has stopped being able to do two jobs at once. The write side wants strict invariants, transactional integrity, and a normalized shape that protects the truth. The read side wants denormalized documents, wide fan-out, and query shapes that have nothing to do with how the data was written. For a while you serve both from one model, and the model slowly turns into a compromise that serves neither well. CQRS, Command Query Responsibility Segregation, is the decision to stop compromising and split the two. Event sourcing is the decision to stop storing the current state at all and instead store the ordered sequence of facts that produced it. The two patterns travel together often enough that people conflate them, but they are separate choices with separate costs, and the central argument of this guide is that you should adopt each one only when the domain earns it.

CQRS and event sourcing on Azure architecture with read and write models, event store, and projections - Insight Crunch

The reason this matters on Azure specifically is that the platform gives you several different ways to build the same shape, and the wrong choice locks in operational pain that no amount of clever code removes later. You can use Azure Cosmos DB and its change feed as both the event store and the engine that drives your read models. You can use Azure Event Hubs as a high-throughput append log. You can route commands through Azure Service Bus, build projections with Azure Functions, and serve queries from a denormalized store sized for reads. Each combination has a different consistency profile, a different failure surface, and a different bill. An architect who picks the combination by reasoning about the workload ends up with a system that scales cleanly. An architect who copies a reference diagram without understanding the eventual consistency it introduces ends up explaining to a product owner why a user who just saved a record cannot see it on the next screen.

What CQRS and event sourcing actually mean

CQRS separates the model that handles writes from the model that handles reads. In a conventional design, one set of classes and one schema serve both the command that changes data and the query that returns it. CQRS says those are different responsibilities with different shapes, so it gives each its own model. The write model accepts commands, enforces business rules, and produces a result. The read model answers queries from a representation optimized for exactly the queries the application asks. The two no longer share a schema, and frequently they no longer share a database. That single split is the whole of CQRS. It does not require event sourcing, it does not require separate services, and it does not require a message bus. At its most modest, CQRS can be two classes and two table designs inside one application talking to one database.

Event sourcing is a different and more radical idea about how state is persisted. A normal system stores the current value of a thing: an account balance is a number, an order has a status column, a shopping cart holds a set of rows. When something changes, you overwrite the old value with the new one, and the previous state is gone unless you went to the trouble of writing an audit log alongside it. Event sourcing inverts this. You never store the current state directly. Instead you store every state change as an immutable event appended to a log: AccountOpened, MoneyDeposited, MoneyWithdrawn, AccountFrozen. The current balance is not a stored number; it is the result of replaying those events in order. The log is the source of truth, and any current-state representation you hold is a derived convenience that you can throw away and rebuild from the events at any time.

How do read and write models actually separate?

The write model owns commands and invariants and produces a result or an event; the read model owns queries and serves a denormalized shape built for them. They stop sharing a schema and often a database, so each scales and changes independently. Writes stay consistent and small; reads stay fast and wide.

The clean way to hold the distinction is that a command expresses intent to change something and can be rejected, while a query asks a question and never changes anything. PlaceOrder is a command; it can fail because the item is out of stock. GetOrderHistory is a query; it returns data and alters nothing. CQRS routes those two kinds of work to two different models because the forces acting on them genuinely differ. A command must validate against the current state, hold invariants such as “an account cannot go below zero,” and usually touch a small, deeply consistent slice of data. A query frequently spans many entities, wants them pre-joined and shaped for display, and tolerates being a little stale far more readily than it tolerates being slow. When one model serves both, every change to a query shape risks the integrity rules and every tightening of an invariant slows a report. Splitting them lets each evolve on its own clock.

Why store events instead of current state?

Because the sequence of events carries information that a current-state snapshot throws away: the order things happened, the intent behind each change, and the full history for audit or temporal queries. You can always derive current state from events, but you can never derive the history from a state you overwrote.

The practical consequence is that event sourcing gives you several capabilities almost for free that are expensive or impossible to bolt on afterward. You get a complete, tamper-evident audit trail because the events are the data, not a side log that can drift from reality. You get temporal queries, the ability to ask what the state was at any past moment, by replaying events up to a point in time. You get the ability to fix a bug in how you interpret events and then rebuild every read model with the corrected logic, recovering a corrected present from an unchanged past. And you get a natural integration point, because the event stream that drives your own read models can also feed other systems that care about the same facts. None of these is free in the engineering sense, but each one is structurally available rather than something you must invent.

The two patterns combine well because event sourcing produces a write side that is naturally append-only and a stream of events that naturally feeds read models, which is exactly the shape CQRS wants. But the combination is not mandatory in either direction. You can do CQRS with two ordinary databases and no events at all, synchronizing them with a change feed or a dual write. You can, in principle, event-source a system without splitting reads and writes, though in practice the replay cost of serving queries directly from events pushes almost everyone toward separate read models. Throughout this guide the assumption is that you are considering the full combination, because that is where the value and the cost both concentrate, but keep the seam between them in mind, because adopting one without the other is often the right amount of complexity for a given domain.

The Azure services that realize the pattern

There is no single Azure product called “the event store,” and that absence is where most design confusion begins. You assemble the pattern from primitives, and the primitives you choose determine the consistency, throughput, retention, and cost you will live with. Four building blocks cover almost every CQRS and event sourcing system on Azure: a durable append-only store for events, a mechanism that reacts to new events, compute that builds projections, and a query store sized for reads. Sometimes one service plays two of those roles, which is why the menu looks shorter than the responsibilities it covers.

The event store: Cosmos DB, Event Hubs, or a dedicated store

Azure Cosmos DB is the most common choice for the event store, and it earns the position for a concrete reason: its change feed turns the same container that holds your events into a reliable, ordered stream that downstream processors can read. You model each event as a document, partition the container by the aggregate or stream identifier so all events for one entity land in the same logical partition and preserve their order, and write events as inserts only. The change feed then exposes those inserts to consumers in the order they were processed within each partition. Because you never update or delete, the container behaves as a true append log, and Cosmos DB gives you horizontal scale and multi-region distribution on top of it. The official Azure documentation describes the change feed as a strong fit for event sourcing precisely because all ingestion is modeled as writes with no updates or deletes.

The detail that trips people is the change feed mode. In latest-version mode, the feed surfaces the most recent change for an item, and if you write several updates to the same item quickly you can miss the intermediate ones. For a pure event store this is rarely a problem because each event is a distinct, immutable document rather than an update to an existing one, so there is no “intermediate version” to lose. But the moment someone mutates an event document in place, the model breaks quietly, which is one more reason the append-only discipline is not optional. There is also an all-versions-and-deletes mode that captures every change including deletes, useful when you genuinely need to observe mutations, though it carries its own retention and configuration constraints that you should verify against the current Cosmos DB documentation before you depend on them.

Azure Event Hubs is the second common event store, and it suits a different shape of problem. Event Hubs is a partitioned, log-based ingestion service built for very high throughput, the kind of firehose where millions of events per period arrive from devices, clickstreams, or telemetry. It keeps events for a configurable retention window and lets multiple consumer groups read the same partitions independently at their own offsets. Where Cosmos DB gives you a queryable store that also streams, Event Hubs gives you a streaming pipe with bounded retention. That difference is the whole decision. If your events must live forever as the permanent source of truth and you also want to query them directly, Cosmos DB fits. If your events are a high-volume stream that you process into durable read models and you do not need to keep the raw events indefinitely in the same system, Event Hubs fits, often with Event Hubs Capture writing the raw stream to Azure Blob Storage or Azure Data Lake for long-term retention.

A dedicated event store, meaning a purpose-built product or a hand-rolled store on top of a relational or document database, is the third option and the one to reach for last. Purpose-built event stores model streams, optimistic concurrency on a stream, and subscriptions as first-class concepts, which removes a lot of plumbing you would otherwise write yourself on Cosmos DB. The cost is another operational surface to run, secure, and patch, and on Azure that usually means hosting it yourself on virtual machines or containers rather than consuming a managed service. The honest default for most teams is Cosmos DB for systems where events are the permanent truth and Event Hubs for systems where events are a high-throughput stream, with a dedicated store reserved for domains rich enough in stream semantics to justify the extra moving part.

How do I store events in Cosmos DB without breaking ordering?

Partition the container by the stream or aggregate identifier so every event for one entity shares a logical partition, and write events as inserts with a monotonically increasing sequence number per stream. The change feed then preserves order within each partition, which is the order that matters for replay.

The sequence number does double duty. It gives you a deterministic replay order within a stream, and it gives you a concurrency check: when you append an event, you expect the next sequence number to be one greater than the last you read, and if another writer got there first the numbers collide and you can reject the write and retry. This is optimistic concurrency on the stream, and it is how event-sourced systems enforce that two commands do not silently overwrite each other’s effect on the same aggregate. Cosmos DB does not give you stream-level concurrency as a built-in feature the way a dedicated event store does, so you implement it with the sequence number and a uniqueness constraint or a conditional write, and you accept that a small amount of retry logic lives in your command handler.

The reactive mechanism: change feed and triggers

Once events are in the store, something has to notice them and act. On Cosmos DB the canonical mechanism is the change feed processor, which reads the feed, tracks each consumer’s position in a lease container, and distributes work across multiple processor instances so the load balances as you scale out. The change feed processor offers at-least-once delivery, which is the single most important property to internalize because it shapes everything downstream: a given event can be delivered to your projection logic more than once, so every projection must be idempotent. Applying the same MoneyDeposited event twice must not double the balance.

Azure Functions wraps the change feed processor in a trigger, which is the path most teams take because it removes the worker infrastructure entirely. You write a function with a Cosmos DB trigger, point it at the monitored container and a lease container, and the platform invokes your function with batches of changed documents as they arrive, scaling the function instances against the partition load for you. The trigger uses latest-version change feed mode and, at present, works with the API for NoSQL only, both constraints worth confirming against current documentation because the supported surface expands over time. For an Event Hubs source the equivalent is the Event Hubs trigger, which hands your function batches of events per partition and checkpoints progress so a restart resumes where it left off rather than reprocessing from the beginning.

The projection compute and the read store

The compute that turns events into read models can be Azure Functions, a hosted service such as Azure Container Apps or Azure Kubernetes Service running a change feed processor, or a stream processor for the highest volumes. The choice follows the same logic as any compute decision on the platform: Functions for event-driven, bursty, scale-to-zero workloads where you want no infrastructure; Container Apps or AKS for projections that need long-lived connections, heavier per-event work, or tighter control over scaling and networking. The projection logic itself is small and disciplined: receive an event, load or initialize the affected read model, apply the change idempotently, and persist the read model.

The read store is whatever serves your queries best, and the freedom to choose it independently of the write side is one of the concrete payoffs of the split. A read model that backs a product catalog page might live in Cosmos DB as denormalized documents. A read model that powers full-text search might be Azure AI Search. A read model that feeds dashboards might be a relational store or a dedicated analytics store. You can run several read models off the same event stream at once, each shaped for its own consumer, and you can add a new one later by replaying the existing events through new projection logic. The write side never has to know any of these exist, which is the loose coupling the pattern is built to deliver.

A reference design walked through: the InsightCrunch CQRS map

Abstract descriptions of CQRS leave people unsure where the boundaries actually fall, so the rest of this guide builds on one concrete reference design and names it so you can hold it as a single picture. Call it the InsightCrunch CQRS map. It traces a single command from the moment a user acts to the moment a different user can read the result, and it marks the one place where consistency stops being immediate. Every trade-off later in the article refers back to a row in this map.

The map has five stages and one boundary. A command enters and is validated by the write model. The write model appends one or more events to the event store. The event store’s change feed emits those events. A projection consumes them and updates a read model. A query reads from the read model. The boundary, the eventual-consistency line, sits between the event being stored and the read model reflecting it. Everything to the left of that line is immediately consistent; everything that crosses it is eventually consistent. The single most common production surprise in these systems is a user or a developer expecting read-after-write consistency across a boundary that the architecture deliberately made asynchronous.

Stage What happens Azure service Consistency
1. Command intake Validate intent against current state, enforce invariants API on App Service, Container Apps, or Functions; optional Service Bus queue Strong within the aggregate
2. Event append Write immutable events to the stream with a sequence number Cosmos DB container (or Event Hubs) Strong, optimistic concurrency on the stream
3. Event emission New events surfaced in order per partition Cosmos DB change feed (or Event Hubs consumer group) At-least-once delivery
4. Projection Apply events idempotently to build or update the read model Azure Functions, Container Apps, or AKS worker Eventually consistent (the boundary)
5. Query Serve denormalized data shaped for the screen Cosmos DB, Azure AI Search, SQL, or a cache Strong read of a possibly stale model

Read the rightmost column as the contract you are signing. Stages one and two are transactional and immediate: when the command returns success, the events are durably stored and the invariants held. Stage three is where asynchrony begins, and stage four is where it bites, because the read model lags the event store by however long it takes the projection to run. Stage five reads that read model strongly, meaning the query returns a consistent answer, but the answer reflects the world as of the last projection, not as of the most recent write. The gap is usually milliseconds to a couple of seconds in a healthy system, but it is never zero, and designing as though it were zero is the root of most CQRS pain.

Walking a command through the map

Take a concrete command in an order-management domain: a customer submits PlaceOrder. The request hits an API hosted on Azure Container Apps. The command handler loads the customer’s recent order stream from the event store to check invariants, perhaps that the customer has no unpaid prior order over a threshold, and validates the cart against current inventory rules. This load-and-check happens against the write side and is strongly consistent, because the events that matter for the decision are read directly from the stream, not from a possibly stale read model. If validation passes, the handler appends an OrderPlaced event, and possibly an InventoryReserved event, to the Cosmos DB container, partitioned by the order or customer identifier, with the next sequence number for that stream. If a concurrent command already wrote that sequence number, the conditional write fails, and the handler retries by reloading the stream and revalidating.

The instant those events are committed, the command returns success to the customer. Their order is real and durable. But the order does not yet appear in the “my orders” list, because that list is a read model and the projection has not run. The change feed surfaces the OrderPlaced event within a short window. An Azure Function with a Cosmos DB trigger receives it, looks up the customer’s order-list read model, appends the new order in a denormalized shape that the list page can render without joins, and writes it back. Now a query against the order-list read model returns the new order. The elapsed time from command success to read-model visibility is the width of the eventual-consistency boundary for this flow, and it is the number you must measure, communicate, and design the user experience around.

What does the eventual-consistency boundary mean for the user?

It means a user can complete an action successfully and then not see it on the next read, for as long as the projection lag lasts. The write succeeded and is durable; the read model simply has not caught up. The fix is never to make the read synchronous but to design the experience so the gap is invisible or expected.

The way mature systems hide the boundary is worth stating plainly because it is where good CQRS user experience is won or lost. The client that issued the command already knows the result of the command, because the command returned it, so the screen that follows a write should render from the command result rather than immediately querying the read model. The order confirmation page shows the order the command just created, not a freshly queried order list. By the time the user navigates somewhere that genuinely reads the projection, the projection has almost always caught up. When it has not, you can show a gentle “processing” state rather than a blank result, and you can have the client poll or subscribe for the read model to converge. What you must not do is paper over the boundary by making the projection synchronous, because that throws away the independent scaling and resilience that justified the split in the first place, and it reintroduces the coupling the pattern existed to remove.

Where the map changes for high throughput

The map’s services swap out when volume climbs, but its shape holds. At very high ingestion rates the event store becomes Event Hubs rather than Cosmos DB, the reactive mechanism becomes an Event Hubs consumer group with checkpointing rather than the Cosmos DB change feed, and the projection compute is more likely to be a long-lived Container Apps or AKS worker, or a stream processor, than a per-event Function invocation. The read store and the boundary stay exactly where the table puts them. Recognizing that the five stages are invariant while the service in each cell is negotiable is what lets you reason about a new system quickly: identify the five responsibilities, then choose a service for each by its throughput, retention, and consistency profile rather than by which reference diagram you saw last.

The trade-offs and the failure modes the pattern must handle

Every capability the pattern grants arrives with a cost that you pay in operational complexity, and a CQRS and event sourcing system that ignores these costs does not fail loudly on day one. It fails quietly weeks later when a projection falls behind, an event format changes, or a rebuild takes longer than anyone budgeted. Naming the failure modes in advance is the difference between designing for them and discovering them in production.

Eventual consistency is a feature you must manage, not a bug you can fix

The lag between a write and the read model reflecting it is intrinsic. You chose asynchrony when you split the models, and you cannot have the independent scaling and resilience of a separate read side and also have the read side update inside the write transaction. The mistake is trying to close the gap rather than manage it. Teams reach for synchronous projection, updating the read model in the same transaction as the event append, and in doing so they recouple the two sides, lose the ability to add read models without touching the write path, and reintroduce the single point of contention they were escaping. The correct posture is to measure the lag, alarm on it when it exceeds a threshold, and design the user experience so the typical lag is invisible. Render post-write screens from command results, show pending states where a fresh read is unavoidable, and let clients converge on the read model rather than blocking on it.

There is a subtler consistency trap on the write side itself. Because the command handler validates against the event stream, two commands against the same aggregate must not both succeed if they conflict, and the optimistic concurrency check on the sequence number is what prevents that. Skip the concurrency check and you get lost updates: two withdrawals each read a balance, each decide it is sufficient, and both commit, overdrawing an account that the invariant said could never go negative. The check is cheap to implement and catastrophic to omit, and it is the most common correctness bug in hand-built event stores on Cosmos DB.

Projections lag, and you must see the lag

A projection is a consumer of the event stream, and like any consumer it can fall behind. On Cosmos DB the change feed processor exposes an estimator that tells you how far each lease is behind the latest change, and you should treat that estimate as a first-class metric, sending it to Azure Monitor and alarming when it crosses a level that your user experience can tolerate. On Event Hubs the equivalent signal is consumer lag against the partition’s latest offset. A projection falls behind for ordinary reasons, a deployment that paused processing, a spike in event volume, a slow downstream write, and the danger is not the lag itself but invisible lag, where the read model is minutes stale and nobody knows because nobody is watching the estimator.

When a projection does fall behind, the architecture’s saving grace is that it catches up on its own as long as the events are retained and the processor is healthy, because the events are still in the store and the lease simply advances through them. This is why retention policy on the event store is a correctness concern, not just a cost concern. If your Event Hubs retention is shorter than the longest plausible projection outage, a projection that falls behind past the retention window loses events permanently and the read model becomes wrong in a way that only a rebuild from a longer-lived archive can fix. Cosmos DB used as the permanent event store sidesteps this because the events live indefinitely, which is one of the reasons it is the safer default for the source of truth.

Poison events and the idempotency requirement

Because delivery is at-least-once, a projection can receive the same event more than once, and because real systems contain bugs and bad data, a projection can receive an event it cannot process. Both have standard handling that you must build in from the start. Idempotency handles duplicates: the projection records the highest event sequence number it has applied for each read model, and when an event arrives with a sequence number it has already processed, it skips the event rather than applying it again. This makes redelivery harmless, which is exactly what at-least-once delivery requires.

A poison event, one that throws every time the projection tries to apply it, is more dangerous because a naive processor retries it forever and blocks every event behind it, freezing the projection. The handling is to detect repeated failure on a single event, move it aside to a dead-letter location with enough context to investigate, and let the projection continue past it. On Service Bus this is the native dead-letter queue. On a Functions-based change feed projection you implement it: catch the failure, write the failed event and its error to a dead-letter store, and acknowledge progress so the lease advances. The trade-off is that the read model is now knowingly incomplete with respect to the dead-lettered event, which is correct, because a knowingly-incomplete model you can repair beats a frozen model that has stopped serving everyone.

How do projections and rebuilds work?

A projection applies events in order to build a read model; a rebuild discards that read model and replays the events from the beginning through the projection logic to recreate it. Rebuilds let you fix projection bugs, add new read models, and change read shapes, all from the unchanged event history.

The rebuild capability is the quiet superpower of event sourcing and the reason the pattern tolerates so much complexity elsewhere. Because the events are the source of truth and the read model is derived, any read model is disposable. If you find a bug in how a projection interpreted events, you fix the projection code, throw away the corrupted read model, and replay the entire event history through the corrected logic, recovering a correct present from a past you never had to touch. If product wants a new view of existing data, you write a new projection and replay the existing events through it, and the new read model materializes without any change to the write side or any migration of live data. This is structurally impossible in a system that overwrote its state, because the information needed to build the new view was discarded at each write.

Rebuilds are not free, and treating them as routine requires planning for their cost. Replaying months of events through a projection takes time proportional to the event count and the per-event work, and during a naive rebuild the read model is unavailable or stale. Production-grade rebuilds therefore build the new read model alongside the live one, replaying into a fresh store while the old read model keeps serving, and then atomically switch reads to the new model once it has caught up to the present. This blue-green approach to projections turns a scary, downtime-laden operation into a routine one, and the systems that rebuild fearlessly are the ones that designed the side-by-side rebuild path before they needed it.

Snapshots keep replay from getting slow

For an aggregate with a long event history, replaying every event to rebuild its current state on the write side gets slow, because loading the aggregate to validate a command means reading and folding thousands of events. Snapshots solve this. Periodically you persist the folded current state of an aggregate as of a given sequence number, and to load the aggregate you read the latest snapshot and then replay only the events after it. The snapshot is an optimization, not a new source of truth, so it must always be reconstructable from the events and never the thing you edit directly. Get this wrong, treat the snapshot as authoritative and mutate it, and you have quietly abandoned event sourcing while keeping its machinery. Get it right and the write side stays fast no matter how long a stream grows.

Event schema evolution is forever, because events are forever

The hardest long-term cost of event sourcing is one nobody feels on day one: your events are immutable and permanent, so the shape of an event you wrote two years ago must still be readable by today’s code. You cannot run a migration that rewrites old events into a new shape, because rewriting the history violates the entire premise that the log is an unalterable record of what happened. You instead handle versioning by making your projection and aggregate logic tolerant of every event version that has ever existed. A new field gets a sensible default when an old event lacks it. A renamed concept gets an upcaster, a small function that reads an old event version and transforms it into the current shape in memory at read time, never on disk. A genuinely incompatible change gets a new event type rather than a mutated old one. This discipline is not glamorous and it accumulates, and underestimating it is the most common reason teams regret event sourcing two years in. Budget for it from the first event you design, keep events small and explicit, and resist the urge to encode rich, fast-changing structures that you will have to support reading forever.

When the pattern fits and when it is overkill

Here is the claim this entire guide is built to defend, the complexity-needs-justification rule: CQRS and event sourcing add real, permanent complexity to a system, the eventual-consistency boundary, the projection machinery, the rebuild planning, and the forever-cost of event versioning, so they pay off only in domains with a genuine need for audit history, temporal queries, or a sharp asymmetry between read and write workloads, and they are a net loss when applied to ordinary create-read-update-delete data as a default. The pattern is a tool with a specific shape, and the failure mode is not using it badly but using it where its shape does not fit.

The strongest signal that the pattern fits is a domain where the history is the point. Financial ledgers, where every transaction must be auditable and the balance is meaningfully the sum of its movements, are the canonical fit, because the events are not an implementation detail of the balance, they are the business reality and the balance is the derived view. Domains with regulatory audit requirements, where you must prove not just the current state but how it was reached and when, fit for the same reason. Domains with rich temporal questions, “what did this look like last Tuesday,” “how often does this entity change state,” “replay this account’s life to debug a dispute,” fit because event sourcing answers those questions natively while a state-overwriting system cannot answer them at all. And domains with a severe read-write asymmetry, where reads outnumber writes by orders of magnitude or where read shapes proliferate independently of the write model, fit the CQRS half of the pattern even if they never adopt event sourcing.

When is CQRS worth the complexity?

CQRS is worth it when the read and write workloads have genuinely different shapes or scaling needs, or when you need multiple independent read models from one write model. It is overkill for simple CRUD where one model serves both reads and writes cleanly, because the split adds machinery and an eventual-consistency boundary you gain nothing from.

The clearest anti-pattern is adopting the full pattern for simple CRUD because an article or a conference talk made it sound like the modern way to build software. A system whose data is a handful of entities with straightforward reads, no audit requirement, no temporal questions, and reads and writes of similar shape and volume gains nothing from CQRS and loses a great deal: developers now reason about two models, an eventual-consistency boundary surprises every new team member, and a simple change that would have been one update touches a command, an event, a projection, and a read model. The complexity is real and the benefit is absent, which is the precise definition of over-engineering. The reasonable default for most line-of-business data remains a single model over a relational or document store, and the burden of proof sits on the side of adding CQRS, not on the side of keeping things simple.

A useful middle position is partial adoption, and it is underused because the pattern is usually presented as all-or-nothing. You can adopt CQRS without event sourcing, keeping a conventional write database and synchronizing a separate read store from it, which buys independent read scaling and shaping without the forever-cost of event versioning. You can adopt event sourcing for the one aggregate in your system that genuinely needs an audit trail while leaving the rest of the system as ordinary CRUD, rather than event-sourcing everything because you event-sourced something. The granularity of the decision is the aggregate, not the application, and the architects who get the most value treat the pattern as something they apply surgically to the parts of the domain that earn it rather than as a style they impose everywhere. The trade-off framing in the Azure Well-Architected Framework, which treats reliability and performance as things you buy at a cost rather than maximize blindly, applies cleanly here: complexity is a cost, and you spend it where it returns value. That reasoning is developed further in the guide to making deliberate architecture trade-offs with the Azure Well-Architected Framework.

The relationship to event-driven architecture and messaging

CQRS and event sourcing sit inside the broader family of event-driven systems, and confusing the members of that family is a common source of muddled design. An event in event sourcing is a fact about a single aggregate, stored as the source of truth, primarily for the system’s own use in rebuilding state. An event in a general event-driven architecture is often a notification published for other systems to react to, and it may not be stored as truth at all. The two overlap when your event stream doubles as both the source of truth and the integration feed, which is efficient but also where coupling can creep back in if downstream systems start depending on the internal shape of your domain events. Keeping internal domain events separate from the integration events you publish for others is the discipline that keeps event sourcing from leaking your domain model across system boundaries. The wider set of choices here, Event Grid for reactive notifications, Event Hubs for streams, Service Bus for commands and work queues, is laid out in the guide to designing event-driven architecture on Azure, and the delivery-guarantee details that make idempotency mandatory are covered in the treatment of asynchronous messaging patterns on Azure.

How to evolve the pattern over time

A CQRS and event sourcing system is rarely built whole on day one, and the systems that age well are the ones that grew into the pattern deliberately rather than committing to the full shape before they understood the domain. The sane evolution path starts narrow and widens only as the domain proves it needs the width.

Begin with a single model and a conventional store. Resist the pattern until a real force appears: a read workload that genuinely fights the write workload, an audit requirement the business will actually enforce, a temporal question someone keeps asking that the current schema cannot answer. When that force appears for one part of the domain, adopt the pattern for that aggregate alone. If the force is a read-shape or read-scale problem, add CQRS first, splitting a read model off the existing write store and synchronizing it through a change feed, which gets you independent read scaling without the event-versioning commitment. If the force is an audit or temporal problem, adopt event sourcing for that aggregate, modeling its events explicitly while leaving everything else as it was.

As the event-sourced part of the system matures, the next evolution is usually the read side multiplying. The first read model serves one screen; the second serves search; the third feeds analytics. This is the pattern working as designed, and it is cheap to add read models precisely because the write side does not change. The discipline that keeps this healthy is making each projection idempotent and rebuildable from the start, so that adding the third read model is a matter of writing a projection and replaying history, not a migration. The systems that struggle at this stage are the ones that built the first projection without the rebuild path, then discovered they could not safely add the second without downtime.

The event store itself evolves along the throughput axis. A system that starts on Cosmos DB with modest volume and grows into a firehose may move its highest-volume streams to Event Hubs while keeping lower-volume, query-heavy streams on Cosmos DB, accepting a mixed event store because the throughput profiles genuinely differ. Getting the Cosmos DB side right, the partition key on the stream identifier, the request-unit budget for the append and the change feed, the indexing policy tuned for the access pattern, repays attention, and the partition-key and throughput reasoning that governs it is covered in depth in the Azure Cosmos DB engineering guide. The point of staging the evolution this way is that you never pay for complexity before the domain forces it, and when it does force it, you add exactly the increment the force requires.

For teams that want to build and break this pipeline safely before committing it to production, you can run the hands-on Azure labs and command library on VaultBook to model commands, append events to a Cosmos DB container, drive projections through the change feed, and watch the eventual-consistency boundary behave under load, which is the fastest way to develop intuition for where the lag lives and how the rebuild path works.

Real-world scenarios and the judgment each one requires

The textbook description of CQRS and event sourcing is clean, and the systems engineers actually run are not, so it helps to walk the recurring scenarios that teams report and name the judgment each one demands. Each is a pattern with a decision attached, not a rule to apply blindly.

The audit-trail win is the scenario that most often justifies the pattern outright. A team in a regulated domain needs to prove not just an account’s current state but every change that produced it, who triggered it, and when. Bolting an audit log onto a state-overwriting system always drifts, because the log and the state are written separately and eventually disagree. Event sourcing removes the drift by construction: the events are the state’s history, so there is nothing to reconcile and nothing to forge. The judgment here is to confirm the audit requirement is real and enforced rather than aspirational, because a genuine regulatory or financial audit need is one of the few forces strong enough to justify the pattern’s full cost on its own.

The independent-read-scaling scenario justifies the CQRS half without necessarily invoking event sourcing. A product surface gets ten thousand reads for every write, and the reads want denormalized, pre-joined documents that the normalized write schema cannot serve without expensive joins under load. Splitting the read model lets you scale the read store and shape its data for the queries while the write store stays small, normalized, and consistent. The judgment is whether the asymmetry is real and persistent, because a read-write ratio that looks dramatic in a demo but evens out in production does not justify the split, while a genuine and growing asymmetry does.

The projection-rebuild scenario is where the pattern earns trust over time. A bug ships in a projection and corrupts a read model for a week before anyone notices. In a state-overwriting system this is a data-recovery incident with backups, partial restores, and lost changes. In an event-sourced system it is a routine fix: correct the projection, rebuild the read model from the unchanged events alongside the live one, and switch reads to the corrected model. The judgment is to have built the side-by-side rebuild path in advance, because a rebuild capability you designed for is a calm afternoon and a rebuild you improvise under pressure is a long night.

The eventual-consistency surprise is the scenario that bites teams who adopted the pattern without internalizing the boundary. A user saves a change, navigates to a list, and does not see it, files a bug, and an engineer spends a day “fixing” a system that is working exactly as designed. The judgment is preventive and lives in the user experience, not the data layer: render post-write screens from command results, show explicit pending states where a fresh read is unavoidable, and educate the team that the boundary is a deliberate contract rather than a defect. Reaching for synchronous projection to make the bug report go away trades the pattern’s core benefit for a symptom’s disappearance and is almost always the wrong call.

The over-engineering scenario is the failure that wears the costume of sophistication. A team applies CQRS and event sourcing to a straightforward administrative module with a dozen entities, no audit need, and symmetric read and write loads, because the pattern signaled seniority. Six months later, simple changes are slow because every one touches four layers, onboarding is harder because new engineers must learn the eventual-consistency model before they can ship a form, and nobody can articulate a benefit the system actually receives. The judgment is the one this whole guide argues for: the complexity must be justified by a real domain force, and absent that force the simple single-model design is not a lesser choice, it is the correct one.

The choose-an-event-store scenario is the decision that sets the system’s ceiling. A team commits to event sourcing and must pick where events live, and the choice determines retention, throughput, query ability, and operational burden for years. The judgment is to match the store to whether events are the permanent truth you also query, in which case Cosmos DB and its change feed fit, or a high-volume stream you process into durable models, in which case Event Hubs with Capture to long-term storage fits, reserving a dedicated event store for domains whose stream and subscription semantics are rich enough to repay the extra operational surface. Choosing by throughput, retention, and query needs rather than by familiarity is what keeps the decision from becoming a regret.

Building the write model and command handling in practice

The write model is where correctness lives, and it is worth slowing down on the mechanics because the bugs that hurt most originate here. A command handler does four things in sequence: it loads the current state of the aggregate it is about to change, it validates the command against that state and the domain invariants, it produces the resulting events, and it appends those events to the stream with a concurrency check. Each step has a way to go wrong that the architecture must guard against.

Loading the aggregate means reading its event stream and folding the events into the current state in memory. For a short stream this is reading every event and applying each one to an in-memory representation; for a long stream it is reading the latest snapshot and then only the events after it. The fold must be deterministic and depend only on the events, never on the wall clock or external state, because the entire premise is that the same events always produce the same state. A fold that calls the current time or reads another service quietly destroys reproducibility, and a rebuild months later produces a different result than the original, which is the kind of bug that takes days to find because it only appears on replay.

Validation runs against the freshly folded state, not against a read model, and this is the consistency guarantee that makes commands safe. The read models are eventually consistent and therefore unsafe to validate against, because a command that checks a stale read model can violate an invariant the read model has not caught up to. The write side reads its own events directly and is strongly consistent within the aggregate, so the invariant check is sound. When a command spans multiple aggregates and needs an invariant across them, you are at the edge of what a single transaction can hold, and the honest answer is usually a process manager or saga that coordinates the aggregates with compensating actions rather than a distributed transaction, a topic that belongs to the messaging patterns the system rides on.

The append is where optimistic concurrency does its work. A minimal Cosmos DB append for the NoSQL API, written in a way that enforces one event per sequence number per stream, looks like this:

// Append a new event with optimistic concurrency on (streamId, sequence).
// The unique key policy on the container enforces (streamId, sequence) uniqueness,
// so a concurrent writer that grabbed the same sequence number causes a conflict.
public async Task AppendAsync(string streamId, long expectedNextSequence, DomainEvent e)
{
    var doc = new EventDocument
    {
        Id = $"{streamId}:{expectedNextSequence}",
        StreamId = streamId,
        Sequence = expectedNextSequence,
        Type = e.GetType().Name,
        SchemaVersion = e.SchemaVersion,
        Data = e,
        Timestamp = DateTimeOffset.UtcNow
    };

    try
    {
        await _container.CreateItemAsync(
            doc,
            new PartitionKey(streamId),
            new ItemRequestOptions { EnableContentResponseOnWrite = false });
    }
    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
    {
        // Another command appended this sequence first. Reload the stream and retry.
        throw new ConcurrencyConflictException(streamId, expectedNextSequence);
    }
}

The shape that matters is the conflict handling. When two commands race on the same aggregate, both compute the same next sequence number, and only one CreateItemAsync succeeds because the identifier or the unique key collides. The loser catches the conflict, reloads the stream to see the event the winner wrote, revalidates its command against the new state, and either retries or fails cleanly. This retry loop is the entire mechanism that prevents lost updates, and it is small enough that there is no excuse for skipping it. The cost is that a hot aggregate, one that many commands hit at once, becomes a contention point, which is a signal either to model the aggregate more finely so commands touch different streams or to serialize commands for that aggregate through a queue so they do not race in the first place.

What happens when two commands hit the same aggregate at once?

The optimistic concurrency check on the sequence number lets exactly one of them win. The loser sees a conflict, reloads the stream to observe the winning event, revalidates its command against the updated state, and retries or fails. This is how event-sourced systems prevent two conflicting writes from both succeeding.

When contention on a single aggregate becomes the bottleneck, the usual remedies are to make the aggregate smaller so that commands which currently collide actually touch independent streams, or to route all commands for a given aggregate through a single ordered queue so they process one at a time rather than racing. Azure Service Bus with message sessions gives you exactly this ordered, single-consumer-per-session processing, which is one reason commands often enter through a queue rather than hitting the write model directly from the API. Routing commands through a durable queue also smooths bursts, gives you retry and dead-lettering for the command itself, and decouples the API’s availability from the write model’s, at the cost of turning command acceptance into its own small asynchronous step that the client must account for.

Building projections in practice

A projection is a function from a stream of events to a read model, and its discipline is narrower than the write model’s but no less important. A change-feed-triggered projection on Azure Functions for the Cosmos DB NoSQL API has a recognizable shape: the trigger hands you a batch of changed documents, you process each one idempotently, and you let the platform checkpoint the lease when your function returns successfully.

[FunctionName("OrderListProjection")]
public async Task Run(
    [CosmosDBTrigger(
        databaseName: "events",
        containerName: "stream",
        Connection = "CosmosConnection",
        LeaseContainerName = "leases",
        CreateLeaseContainerIfNotExists = true)] IReadOnlyList<EventDocument> events,
    ILogger log)
{
    foreach (var e in events)
    {
        // Idempotency: skip any event already applied to this read model.
        var readModel = await _reads.LoadOrInitAsync(e.StreamId);
        if (e.Sequence <= readModel.LastAppliedSequence)
            continue;

        try
        {
            Apply(readModel, e);                 // pure, deterministic state change
            readModel.LastAppliedSequence = e.Sequence;
            await _reads.SaveAsync(readModel);
        }
        catch (Exception ex)
        {
            // Poison event: move aside, record context, let the lease advance.
            await _deadLetter.WriteAsync(e, ex.ToString());
            log.LogError(ex, "Dead-lettered event {Id}", e.Id);
        }
    }
}

Three properties make this projection safe. It is idempotent, because it records the highest sequence it has applied per read model and skips anything at or below that, so redelivery under at-least-once semantics changes nothing. It tolerates poison events, because a single event that throws is moved to a dead-letter store rather than retried forever, which keeps one bad event from freezing the whole projection behind it. And its Apply method is pure, depending only on the read model and the event, which keeps the projection rebuildable: replaying the same events produces the same read model every time.

The thing the code does not show, because it is operational rather than written in the handler, is the lease container’s role. The change feed processor stores each consumer’s position in the lease container, and that position is what lets the projection resume after a restart and what the lag estimator reads to tell you how far behind it is. Treat the lease container as part of the system, back it up, do not share one lease container across unrelated projections, and give each distinct projection its own processor name so the platform tracks their positions independently. A second projection with its own lease reads the same event stream from its own offset without interfering with the first, which is exactly how you run several read models off one event source at once.

Why must every projection be idempotent?

Because the change feed and Event Hubs both deliver at-least-once, so a projection will eventually see the same event more than once, after a restart, a rebalance, or a retry. An idempotent projection that tracks the last applied sequence and skips duplicates makes redelivery harmless, which is the only safe design under at-least-once delivery.

The temptation to chase exactly-once delivery and skip the idempotency work is strong and always a mistake on these platforms, because exactly-once delivery across a distributed boundary is not something the infrastructure provides. What you can build is exactly-once effect, which is a different and achievable thing: deliver at-least-once and make the effect idempotent so that duplicate deliveries produce no duplicate change. The sequence-number check is the simplest form, and for projections that perform external side effects rather than just updating a read model, you extend the idea with an idempotency key recorded in the same transaction as the effect so that a redelivered event finds the key already present and skips the side effect. This effective-exactly-once construction is the backbone of correct event processing, and it is worth building once, carefully, into a shared projection base so that every projection inherits it rather than reimplementing it inconsistently.

Operating the system: what to monitor and how it fails in production

A CQRS and event sourcing system has more moving parts than a single-database application, and the parts fail in ways a single database does not, so the operational model has to watch signals that a conventional system does not even produce. The single most important signal is projection lag, the distance between the latest event in the store and the latest event a projection has applied. On Cosmos DB the change feed processor’s estimator reports this per lease, and it should flow into Azure Monitor with an alert when it exceeds the threshold your user experience can absorb. A read model that is two seconds behind is usually fine; a read model that is two minutes behind during business hours is usually a problem the team needs to know about before users do.

The second signal is dead-letter volume. A projection that is dead-lettering events is telling you that some events cannot be processed, and a rising dead-letter count is an early warning of a schema mismatch, a bug in the projection, or bad data in the stream. Alarm on the rate, not just the presence, because a steady trickle of dead-lettered events that nobody investigates accumulates into a read model that is silently and increasingly wrong. Each dead-lettered event should carry enough context, the original event, the projection that failed, and the error, to diagnose and replay it once fixed, because the recovery path for a dead-lettered event is to correct the cause and feed the event back through the projection.

The third signal is write-side concurrency conflict rate. A low rate of conflicts is healthy and expected, the optimistic concurrency check doing its job. A high rate means a hot aggregate that many commands contend over, and it tells you to either remodel the aggregate so commands touch finer-grained streams or to serialize commands for that aggregate through an ordered queue. Watching the conflict rate turns a vague performance complaint into a specific, actionable diagnosis, which is the difference between tuning and guessing.

Deployment is its own failure surface because a projection paused during a deployment falls behind and then must catch up, and a projection whose logic changed in a deployment may interpret events differently than before. The safe deployment posture treats projection code changes as potential rebuilds: if the change alters how events map to the read model, plan a rebuild into a fresh read model alongside the live one rather than mutating the running model in place, because a code change that reinterprets history applied to a model built under the old interpretation produces a model that is half one logic and half the other. The side-by-side rebuild path you built for bug recovery is the same path you use for logic changes, which is one more reason to build it early.

How do I recover when a projection has corrupted a read model?

You rebuild it. Fix the projection logic, replay the events into a fresh read model alongside the live one, let it catch up to the present, then atomically switch reads to the corrected model and retire the old one. Because the events are the unchanged source of truth, the corrupted read model is disposable and recovery requires no backups.

The reason this recovery is calm rather than frightening is that nothing was lost. The read model was wrong, but the read model was always derived, and the events that the read model was derived from were never touched. Contrast this with a state-overwriting system where a projection bug means the authoritative data itself is corrupt, recovery means restoring backups and replaying transactions from logs, and any changes made after the bug shipped are in jeopardy. The event-sourced recovery is bounded and repeatable because the failure was confined to a derived view. This containment of corruption to disposable views is, alongside the audit trail, one of the two capabilities that most often repay the pattern’s complexity, and it is the reason teams that have lived through a data-corruption incident on a conventional system tend to value event sourcing more than teams that have not.

Reasoning about cost

The cost of a CQRS and event sourcing system splits across the same components as its architecture, and reasoning about it means reasoning about each one rather than looking for a single number. On Cosmos DB the dominant cost is provisioned or consumed request units, and an event store has a specific request-unit profile: writes are frequent small inserts, the change feed read is a steady background consumer, and queries against the event store itself should be rare because queries belong to the read models. Sizing the request-unit budget means accounting for the append rate, the change feed consumption by every projection reading the feed, and any direct stream reads the command handlers perform to load aggregates, which is exactly where snapshots earn their cost by cutting the number of events a load must read.

On Event Hubs the cost model is throughput units or processing units rather than request units, sized to the ingestion and egress rate, plus the storage cost of Capture if you archive the raw stream to Blob Storage or a data lake for long-term retention. The decision between Cosmos DB and Event Hubs as the event store therefore has a cost dimension as well as a consistency and retention dimension: a high-volume stream can be markedly cheaper to ingest on Event Hubs than to write as individual documents on Cosmos DB, while a system that needs to query its events and keep them forever pays Event Hubs twice, once for the stream and once for the long-term archive, where Cosmos DB charges once for a store that does both. None of these are fixed prices to quote, because Azure pricing and the request-unit cost of an operation change, and you should confirm both against the current Azure pricing and the current Cosmos DB request-unit documentation before you size anything.

The projection compute cost follows the compute model you chose. Azure Functions on a consumption plan charges per execution and scales to zero between bursts, which suits projections that process events in spikes and idle in between. Container Apps or AKS charge for the running capacity whether or not events are flowing, which suits projections with steady, heavy per-event work or long-lived connections. The read store cost is the cost of whatever serves your queries, and the freedom to add read models cheaply has a cost tail: every read model is another store to provision and another projection to run, so multiplying read models multiplies the steady-state bill even though each one was cheap to add. The honest cost framing for the whole pattern is that it trades a higher steady-state operational cost for the capabilities of audit, rebuild, independent scaling, and temporal query, and that trade is worth making only when those capabilities have real value, which is the complexity-needs-justification rule restated in the language of the monthly bill.

The mistakes that quietly break the pattern

Most CQRS and event sourcing systems that go wrong do not fail from a single dramatic error but from a handful of small compromises, each of which looks reasonable in isolation and each of which removes a property the pattern depends on. Naming them as a set makes them easy to catch in a design review before they ship.

Mutating an event after it is written is the most fundamental violation, because it breaks the premise that the log is an unalterable record. The moment an event can change, every rebuild becomes unreliable, every audit claim becomes false, and the change feed’s latest-version mode can silently drop the intermediate state. Events are immutable; corrections are new events that supersede the old, never edits to the old. A team that finds itself wanting to update an event is usually missing a CorrectionApplied or Reversed event type that would express the correction as a new fact.

Validating a command against a read model rather than the event stream is the most common correctness bug, because the read model is eventually consistent and a command validated against stale data can break an invariant. The write side must read its own events to validate, accepting the cost of folding the stream so that the invariant check sees the true current state. Skipping this in the name of convenience, “the read model already has the balance, just check that,” reintroduces exactly the race the pattern was supposed to prevent.

Omitting the optimistic concurrency check turns concurrent commands on one aggregate into lost updates, and it is tempting to omit because it works fine until two commands race, which they will under load. Building synchronous projections to dodge the eventual-consistency boundary recouples the read and write sides and discards the independent scaling that justified the split, trading the pattern’s benefit for the disappearance of a user-experience problem that belongs in the user experience layer. Shipping without a rebuild path means the first projection bug becomes a crisis instead of a routine fix, because you cannot safely replay history into a fresh model you never designed to build twice.

Designing fat events, events that carry large, richly structured payloads that change often, multiplies the forever-cost of event versioning, because every shape you ever wrote must remain readable. Keep events small, explicit, and focused on the fact they record. Treating a snapshot as the source of truth rather than a reconstructable optimization quietly abandons event sourcing while keeping its machinery, because once you edit the snapshot directly the events are no longer authoritative. Sharing one lease container across unrelated projections couples their offsets and their failures, so give each projection its own lease and processor name. And skipping dead-lettering lets one poison event freeze an entire projection, which turns a single bad record into a system-wide outage of the read side. Each of these is a small decision, and each one removes a guarantee the pattern exists to provide, which is why the discipline matters more here than in a simpler design where the blast radius of a shortcut is smaller.

A decision table for the three designs

The choice is rarely binary, because between a single shared model and the full CQRS-plus-event-sourcing pattern sits CQRS without event sourcing, and that middle option fits more systems than either extreme. The InsightCrunch design decision table sets the three side by side on the forces that should drive the choice, so you can locate a system rather than default to a style.

Force Single model (CRUD) CQRS without event sourcing CQRS with event sourcing
Read and write shapes Similar, one schema serves both Diverge; reads want denormalized views Diverge, and history matters too
Read-to-write scaling Symmetric Reads dominate or shapes multiply Reads dominate plus audit or temporal need
Audit and history A separate log if needed A separate log if needed Native; events are the history
Temporal queries Not supported without extra work Not supported without extra work Native; replay to any point
Consistency Strong, read-after-write Eventual across the boundary Eventual across the boundary
Added complexity None Moderate; two models, sync mechanism High; events, projections, versioning
Reasonable default for Most line-of-business data Read-heavy or multi-view domains Financial, regulated, or temporal domains

Read the table top to bottom for a candidate system and the right column usually announces itself. If the read and write shapes are similar and scaling is symmetric and there is no audit or temporal need, the leftmost column is correct and adding anything else is over-engineering. If reads dominate or read shapes multiply but you have no need to keep history as truth, the middle column gives you the scaling and shaping benefit without the event-versioning commitment. Only when history itself is part of the domain, when audit, temporal queries, or the ability to rebuild any view from an unchanged past carry real value, does the full pattern in the right column repay its high added complexity. The table is the complexity-needs-justification rule made operational: each row is a force, and the pattern is justified only when enough forces point at the right column.

Which design should a new project start with?

Almost always the leftmost: a single model over one store. Move right only when a concrete force appears, a real read-write asymmetry pushes you to CQRS, a real audit or temporal requirement pushes you to event sourcing. Starting on the right because the pattern looks sophisticated is the most expensive mistake in this space.

The reason starting simple is almost always correct is that you can evolve rightward when a force appears but you cannot easily evolve leftward when you discover the complexity was unjustified. Adding CQRS to a single-model system for one aggregate is a contained change; adding event sourcing to one aggregate is a contained change. But unwinding a fully event-sourced system that never needed it means migrating event streams back into mutable state, rewriting every command handler, and retraining a team that learned to think in events, which almost no team actually does, so they live with the unjustified complexity instead. The asymmetry of the migration cost is the practical argument for the conservative default: the cost of starting too simple is a contained refactor later, while the cost of starting too complex is a permanent tax that compounds with every feature.

The verdict

CQRS and event sourcing are among the most capable patterns available on Azure and among the most often misapplied, and the gap between the two is entirely a matter of judgment about fit. The patterns deliver real and structurally hard-to-replicate value: a write side that holds invariants strongly while read models scale and multiply independently, a complete and tamper-evident history that doubles as the source of truth, the ability to rebuild any view from an unchanged past, and native answers to temporal questions a state-overwriting system cannot answer at all. On Azure you assemble this from primitives, most often Cosmos DB and its change feed as the event store and projection engine, Event Hubs where the events are a high-volume stream, Azure Functions or a hosted worker building idempotent projections, and a read store shaped for each query, with the eventual-consistency boundary sitting deliberately between the event store and the read models.

The cost is equally real and never goes away: the eventual-consistency boundary you must design the user experience around, the projection machinery you must monitor and rebuild, the at-least-once delivery that makes idempotency mandatory, and the forever-cost of event versioning that grows with every event type you ever ship. The complexity-needs-justification rule is the whole of the recommendation: adopt the pattern at the granularity of the aggregate, only where a genuine audit, temporal, or read-write-asymmetry force demands it, and treat the simple single-model design not as a lesser option but as the correct default everywhere else. An architect who can name the force justifying the pattern for a given aggregate is using it well; an architect applying it everywhere because it signals sophistication is paying a permanent tax for capabilities the domain never needed. Build it where it fits, keep it simple where it does not, and the pattern becomes a precise instrument rather than a default that quietly slows every team that adopts it without cause.

Frequently Asked Questions

Q: What are CQRS and event sourcing on Azure, and are they the same thing?

They are two separate patterns that often travel together. CQRS, Command Query Responsibility Segregation, splits the model that handles writes from the model that handles reads, so each gets a shape suited to its job rather than sharing one compromise schema. Event sourcing is a persistence choice: instead of storing current state and overwriting it, you store every state change as an immutable event in an append-only log and derive current state by replaying those events. On Azure you can do CQRS with two ordinary databases and no events at all, and you can event-source a single aggregate without splitting reads and writes across the whole system. They combine well because event sourcing produces an append-only write side and an event stream that naturally feeds read models, which is exactly the shape CQRS wants, but each is an independent decision with its own cost, and conflating them leads teams to adopt more complexity than the domain requires.

Q: How do read and write models separate in a CQRS system?

The write model accepts commands, validates them against current state, enforces business invariants, and produces a result or one or more events. The read model serves queries from a representation shaped specifically for the queries the application asks, usually denormalized so a screen renders without joins. The two stop sharing a schema and frequently stop sharing a database, which lets each scale and change on its own clock. A command expresses intent and can be rejected; a query asks a question and never changes anything. The forces differ: a command touches a small, deeply consistent slice and must hold invariants, while a query often spans many entities, wants them pre-joined, and tolerates mild staleness far better than slowness. Separating them means a change to a query shape no longer risks the integrity rules, and a tightening of an invariant no longer slows a report.

Q: How do I store events in Azure Cosmos DB?

Model each event as an immutable document, partition the container by the stream or aggregate identifier so every event for one entity shares a logical partition and preserves order, and write events as inserts only, never updates or deletes. Give each event a per-stream sequence number, which provides both a deterministic replay order and an optimistic concurrency check: appending expects the next sequence to be one greater than the last read, and a collision means another writer got there first. The change feed then exposes these inserts to consumers in processed order within each partition, which is the order replay depends on. Enforce the sequence uniqueness with a unique key policy or a conditional write so concurrent commands cannot both claim the same sequence. Because you never mutate documents, the container behaves as a true append log, and the change feed’s latest-version mode poses no risk since there are no intermediate versions of an immutable event to lose.

Q: When should I use Event Hubs instead of Cosmos DB for the event store?

Choose by whether the events are permanent truth you also query, or a high-volume stream you process into durable models. Cosmos DB fits when events must live indefinitely as the source of truth and you want a store that both persists and streams them through its change feed, which is the common case for domain event sourcing. Event Hubs fits when events arrive as a firehose, telemetry, clickstreams, device data, at volumes where writing each as a document is expensive, and you process them into read models without needing to keep the raw events forever in the same system. Event Hubs has a configurable retention window rather than indefinite storage, so for long-term retention you pair it with Event Hubs Capture writing the raw stream to Blob Storage or a data lake. The decision has a cost dimension too: a firehose is often cheaper to ingest on Event Hubs, while a query-and-keep-forever store is cheaper on Cosmos DB, which charges once for a store that does both jobs.

Q: How do projections and rebuilds work, and why do they matter?

A projection is a consumer that applies events in order to build a read model, receiving events from the change feed or an Event Hubs consumer group and updating a denormalized view. A rebuild discards a read model entirely and replays the events from the beginning through the projection logic to recreate it. Rebuilds matter because they are the payoff that justifies much of the pattern’s complexity: because events are the source of truth and read models are derived, any read model is disposable. You can fix a projection bug and replay to recover a correct present from an unchanged past, add a brand-new view of existing data by replaying through new projection logic, or change a read shape without touching the write side. Production-grade rebuilds build the new model alongside the live one and switch atomically once it catches up, turning a downtime-laden operation into a routine one, which is why designing the side-by-side rebuild path early is essential.

Q: How do I handle eventual consistency between the write and read sides?

You manage it rather than eliminate it, because the lag is intrinsic to having an independently scaling read side. Measure the projection lag continuously, on Cosmos DB through the change feed processor’s lag estimator, and alarm when it exceeds what your user experience tolerates. Design the experience so the typical lag is invisible: render the screen that follows a write from the command’s own result rather than immediately querying the read model, since the client already knows what the command did. Where a fresh read is unavoidable and the model may not have caught up, show an explicit pending state and let the client poll or subscribe until the read model converges. The mistake to avoid is making the projection synchronous to close the gap, because that recouples the read and write sides and discards the independent scaling and resilience that justified splitting them, trading the pattern’s core benefit to make a user-experience problem disappear from the data layer.

Q: When is CQRS worth the added complexity?

CQRS is worth it when the read and write workloads genuinely diverge, in shape, in scaling needs, or in how often each changes, or when you need several independent read models built from one write model. A read surface that takes orders of magnitude more traffic than writes and wants denormalized documents the normalized write schema cannot serve cheaply is a clear fit. So is a domain where new read views proliferate independently of the write model. It is overkill for straightforward data where one model serves reads and writes cleanly, the entities are few, and the loads are symmetric, because the split adds two-model complexity and an eventual-consistency boundary you gain nothing from. A useful middle ground is CQRS without event sourcing: a conventional write store with a separate read store synchronized through a change feed, which buys independent read scaling and shaping without the forever-cost of event versioning that full event sourcing carries.

Q: Why must every projection be idempotent?

Because both the Cosmos DB change feed and Event Hubs deliver at-least-once, meaning a projection will eventually receive the same event more than once, after a restart, a lease rebalance, or a retry. If the projection is not idempotent, redelivery double-applies the change, so a MoneyDeposited event seen twice doubles the balance. The standard defense is to record, per read model, the highest event sequence number already applied, and skip any event at or below it, which makes redelivery a no-op. For projections that perform external side effects rather than only updating a read model, you extend this with an idempotency key recorded in the same transaction as the effect, so a redelivered event finds the key present and skips the side effect. Chasing exactly-once delivery instead is a mistake, because the infrastructure does not provide it across a distributed boundary; what you build is exactly-once effect on top of at-least-once delivery.

Q: What is a poison event and how do I handle it?

A poison event is one that throws every time a projection tries to apply it, usually because of a bug in the projection, a schema mismatch, or bad data in the event itself. The danger is that a naive processor retries it forever and blocks every event behind it, freezing the projection and leaving the read model frozen in time. The handling is to detect repeated failure on a single event, move it aside to a dead-letter store with enough context to investigate, the original event, the projection that failed, and the error, and then acknowledge progress so the lease advances past it and the projection continues. On Azure Service Bus the dead-letter queue is native; on a Functions-based change feed projection you implement the dead-letter step yourself. The read model is now knowingly incomplete with respect to that event, which is the correct trade, because a knowingly-incomplete model you can repair by fixing the cause and replaying the event beats a frozen model that has stopped serving everyone.

Q: How do snapshots fit into event sourcing on Azure?

Snapshots are an optimization for aggregates with long event histories. Loading an aggregate to validate a command means folding its events into current state, and for a stream with thousands of events that read-and-fold gets slow. A snapshot persists the folded state as of a given sequence number, so loading the aggregate becomes reading the latest snapshot and replaying only the events after it rather than the entire history. The critical discipline is that a snapshot is never a source of truth; it must always be reconstructable from the events, and you must never edit it directly. Treating a snapshot as authoritative and mutating it quietly abandons event sourcing while keeping its machinery, because the events are then no longer the truth. Implemented correctly, snapshots keep the write side fast no matter how long a stream grows, and you regenerate them periodically or after a threshold number of new events, discarding and rebuilding them freely since the events remain the only authoritative record.

Q: How do I evolve event schemas when events are immutable?

You make your code tolerant of every event version that has ever existed, because you can never run a migration that rewrites old events without violating the premise that the log is unalterable. A new optional field gets a sensible default when an old event lacks it. A renamed or restructured concept gets an upcaster, a function that reads an old event version and transforms it into the current shape in memory at read time, never on disk. A genuinely incompatible change becomes a new event type rather than a mutated old one. This versioning cost is permanent and accumulates, which is the most underestimated long-term burden of event sourcing and a common reason teams regret it years in. The defenses are to keep events small and explicit from the first design, avoid encoding rich fast-changing structures you will have to read forever, and budget the versioning work into every change rather than treating it as a one-time cost.

Q: Can I run multiple read models from one event stream?

Yes, and doing so is one of the pattern’s main advantages. Each read model is built by its own projection, which is a separate consumer of the event stream with its own position tracked in its own lease and its own processor name on the Cosmos DB change feed, or its own consumer group on Event Hubs. One projection might build denormalized documents for a product page, another might feed Azure AI Search for full-text queries, and a third might populate a relational store for dashboards, all from the same events without interfering with one another. The write side never knows these read models exist, which is the loose coupling the pattern delivers. You can add a new read model at any time by writing a projection and replaying the existing events through it, so a view product asks for next quarter is built from data already captured rather than requiring a migration. The cost tail is that each read model is another store to provision and another projection to operate and monitor.

Q: What is the relationship between event sourcing and event-driven architecture?

They overlap but are not the same. An event in event sourcing is a fact about a single aggregate, stored as the system’s source of truth, primarily so the system can rebuild its own state. An event in a general event-driven architecture is often a notification published for other systems to react to, and it may not be stored as truth at all. The two meet when one event stream serves as both the internal source of truth and the integration feed other systems consume, which is efficient but risks coupling if downstream consumers start depending on the internal shape of your domain events. The discipline that prevents this is keeping internal domain events separate from the integration events you publish externally, translating between them at the boundary, so event sourcing does not leak your domain model across system lines. Event sourcing is a persistence and modeling pattern; event-driven architecture is an integration style, and a system can use either, both, or neither.

Q: Should I route commands through a queue or call the write model directly?

Both are valid, and the choice turns on contention, burst behavior, and how much asynchrony the client can absorb. Calling the write model directly from the API is simpler and keeps command acceptance synchronous, which suits low-contention aggregates and clients that want an immediate result. Routing commands through a durable queue such as Azure Service Bus smooths bursts, gives the command itself retry and dead-lettering, and decouples the API’s availability from the write model’s. With message sessions, a queue also serializes all commands for a given aggregate through a single ordered consumer, which eliminates the optimistic-concurrency contention on hot aggregates by ensuring commands for one aggregate never race. The cost is that command acceptance becomes its own asynchronous step the client must account for, so the API returns an accepted acknowledgment rather than the command’s final result. For most systems, start with direct calls and introduce a command queue when contention or burst smoothing makes the asynchrony worth it.

Q: How do I monitor a CQRS and event sourcing system in production?

Watch three signals a single-database system does not even produce. The first is projection lag, the distance between the latest event in the store and the latest event a projection has applied, reported by the change feed processor’s lag estimator on Cosmos DB or consumer lag on Event Hubs; send it to Azure Monitor and alarm when it exceeds what the user experience tolerates. The second is dead-letter volume and rate, because a rising count of events a projection cannot process is an early warning of a schema mismatch or bug that is silently making a read model wrong. The third is write-side concurrency conflict rate, where a low rate is healthy and a high rate signals a hot aggregate to remodel or serialize. Beyond these, treat lease containers as part of the system to back up and never share across unrelated projections, and treat projection logic changes as potential rebuilds rather than in-place mutations, because a code change that reinterprets history applied to a model built under the old logic produces a half-and-half model.

Q: Is event sourcing overkill for a typical CRUD application?

Usually, yes. A typical create-read-update-delete application has a handful of entities, reads and writes of similar shape and volume, no regulatory audit requirement, and no temporal questions, and for that profile a single model over a relational or document store is not a lesser choice but the correct one. Applying event sourcing there imposes the eventual-consistency boundary, the projection machinery, the rebuild planning, and the permanent event-versioning cost, while delivering capabilities the domain never uses. The result is that simple changes touch four layers instead of one, onboarding requires teaching the eventual-consistency model before anyone can ship a form, and no one can name a benefit the system receives. The burden of proof sits on the side of adding the pattern, not on keeping things simple. Adopt event sourcing only for the specific aggregates where audit, temporal queries, or rebuild-from-history carry real value, and leave the rest as ordinary CRUD, because the granularity of the decision is the aggregate, not the application.

Q: How does CQRS interact with the Azure Well-Architected Framework’s trade-offs?

It is a direct application of the framework’s central idea that you buy qualities like reliability and performance at a cost rather than maximizing every pillar at once. CQRS and event sourcing buy read scalability, auditability, and the ability to rebuild views, and they pay for those with operational complexity, an eventual-consistency boundary, and a higher steady-state cost. Treating that as a deliberate, documented trade-off rather than a default is exactly the reasoning the framework asks for. The reliability pillar benefits from the containment of corruption to disposable read models and the ability to recover by rebuild rather than restore. The performance-efficiency pillar benefits from independently scalable read models. The operational-excellence pillar bears the cost of more moving parts to monitor. Naming which pillars the pattern strengthens and which it taxes, for a specific workload, turns the adoption decision into a documented architectural trade-off rather than a style choice, which is the posture the framework exists to encourage.

Q: What happens to in-flight commands if a projection or the read store goes down?

Commands are unaffected, which is one of the resilience benefits of the split. The write path, command intake, validation against the event stream, and the event append, depends only on the event store, not on the read models or their projections. If a projection crashes or a read store goes offline, commands keep succeeding and events keep being durably appended, because the write side never waits on the read side. When the projection recovers, it resumes from its last recorded position in the lease and processes the backlog of events that accumulated while it was down, catching up on its own as long as the events were retained, which on Cosmos DB used as the permanent store they always are. This is why retention on the event store is a correctness concern: a projection outage longer than the event retention window on a system using Event Hubs would lose events past that window. The read model is stale during the outage, not corrupt, and it converges once the projection catches up, so users see slightly old data rather than errors.

Q: How do I test a CQRS and event sourcing system?

Test the layers by their distinct responsibilities. Command handlers are highly testable because they are deterministic functions from current state plus a command to events: arrange a stream of prior events, fold them into state, apply the command, and assert on the events produced, including the rejection cases where an invariant blocks the command. This given-events, when-command, then-events style is a natural fit for the pattern and catches invariant bugs cleanly. Projections are equally testable as deterministic functions from events to a read model: feed a sequence of events and assert the resulting read model, and specifically test idempotency by feeding the same event twice and asserting the read model is unchanged the second time. Test the rebuild path explicitly by building a read model, then rebuilding it from the same events and asserting the two match, which proves the projection is pure and the rebuild is safe. Integration tests then verify the wiring, that events appended to the store actually reach the projection through the change feed within an acceptable lag, which is the one part the unit tests cannot cover.