---
title: "20 — Data and Messaging Patterns: CQRS, Event Sourcing, Pub/Sub, and Competing Consumers"
uid: data-and-messaging-patterns
tags: ["cqrs", "competing-consumers", "patterns", "event-sourcing", "pub-sub", "roadmap:system-design", "system-design"]
excerpt: "Data patterns reshape how data is stored and read (CQRS, event sourcing, materialized views); messaging patterns shape how work moves between components (pub/sub, competing consumers, claim check)."
date: 2026-08-13T03:27:30+0000
source: https://www.aveshina.my.id/en/blog/data-and-messaging-patterns
---

"Just use a queue" was how I conflated data and messaging patterns, which live at different layers. Writing them down separated the concerns: **data patterns reshape how data is stored and read (CQRS, event sourcing, materialized views, index tables), and messaging patterns shape how work moves between components (pub/sub, competing consumers, queue-based load leveling, claim check).** [1][2] Both families solve scale problems, but at different layers.

The framing that landed is to ask which layer has the bottleneck. If the bottleneck is "my read shape does not match my write shape," the data-pattern family applies. If the bottleneck is "producers and consumers need to be decoupled," the messaging-pattern family applies. Most large systems use both — a messaging pattern to move events around, and a data pattern to reshape them for efficient reads.

## Data patterns: reshape for reads and writes

These patterns accept that one model rarely serves both writes and reads well at scale, and they split or pre-compute accordingly [1]:

- **CQRS (Command Query Responsibility Segregation)** separates read and update operations for a data store. It maximizes performance, scalability, and security by letting the read and write sides evolve independently, and prevents update commands from causing merge conflicts at the domain level [3]. The write side handles commands; one or more read models are built from the events the write side emits.
- **Event sourcing** stores the full series of actions taken on data in an append-only log, rather than just the current state. The log is the system of record and can be used to materialize domain objects. It simplifies complex domains, improves performance and scalability, and maintains a full audit trail that enables compensating actions [4]. Combined with CQRS, the event log is the write side and the read models are materialized projections of it.
- **Materialized view** generates prepopulated views over data when the data is not ideally formatted for the required queries, supporting efficient querying and improving performance [5]. This is the read-model half of CQRS — a denormalized table built for a specific query shape.
- **Index table** creates indexes over fields frequently referenced by queries, improving query performance by letting applications locate data more quickly [6]. The textbook fix for "this query is slow because it scans the whole table."
- **Sharding** (covered in the database notes) horizontally partitions data across servers for performance, scalability, and availability [7].

```figure
<svg viewBox="0 0 740 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="CQRS + Event Sourcing. Left: a Command enters the Write model, which appends an Event to an append-only Event Store. From the Event Store, one or more Read models are materialized (a projection). Queries read from the materialized read models, never the write side.">
  <defs>
    <marker id="dmarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- write side -->
    <text x="120" y="24" font-size="11" font-weight="700" fill="#7f1d1d" text-anchor="middle">Write side</text>
    <rect x="30" y="40" width="80" height="34" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="70" y="62" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">Command</text>
    <rect x="130" y="40" width="90" height="34" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="175" y="62" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">Write model</text>
    <path d="M110,57 L128,57" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#dmarrow)"/>

    <!-- event store -->
    <rect x="260" y="40" width="120" height="34" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="320" y="58" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">Event Store</text>
    <text x="320" y="70" font-size="8" fill="#500724" text-anchor="middle">append-only log</text>
    <path d="M220,57 L258,57" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#dmarrow)"/>

    <!-- materialize -->
    <text x="500" y="24" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">Read side (materialized)</text>
    <rect x="420" y="40" width="90" height="34" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="465" y="62" font-size="9" font-weight="700" fill="#052e16" text-anchor="middle">Read model A</text>
    <rect x="520" y="40" width="90" height="34" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="565" y="62" font-size="9" font-weight="700" fill="#052e16" text-anchor="middle">Read model B</text>
    <rect x="620" y="40" width="90" height="34" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="665" y="62" font-size="9" font-weight="700" fill="#1e1b4b" text-anchor="middle">Query</text>
    <path d="M380,50 L418,50" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#dmarrow)"/>
    <path d="M380,64 L518,64" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#dmarrow)"/>
    <path d="M510,57 L618,57" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#dmarrow)"/>

    <text x="370" y="110" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">writes append events; reads come from materialized projections</text>

    <!-- supporting data patterns -->
    <text x="40" y="150" font-size="11" font-weight="700" fill="#334155">Supporting patterns</text>
    <g font-size="10" fill="#334155">
      <circle cx="44" cy="170" r="3" fill="#64748b"/><text x="54" y="174">Materialized View — prepopulated read-optimized view</text>
      <circle cx="44" cy="190" r="3" fill="#64748b"/><text x="54" y="194">Index Table — index hot query fields</text>
      <circle cx="44" cy="210" r="3" fill="#64748b"/><text x="54" y="214">Sharding — partition by key for scale</text>
    </g>
  </g>
</svg>
```

The way of thinking for the data family: stop trying to serve every read from the same model that accepts writes. Write once, project many times into read-optimized shapes. The cost is eventual consistency between the write and the read models (a projection lags the log), and the operational complexity of maintaining the projections.

## Messaging patterns: move work between components

These patterns decouple producers from consumers using a messaging substrate [2]:

- **Publisher-Subscriber (pub/sub)** lets an application announce events to multiple interested consumers asynchronously, without coupling senders to receivers [8]. The publisher does not know who is listening; each subscriber gets its own copy. Good for fan-out — one event triggers many independent reactions.
- **Competing consumers** enables multiple concurrent consumers to process messages received on the same channel, optimizing throughput, improving scalability and availability, and balancing the workload [9]. This is the pattern behind a worker pool draining one queue — parallelism for throughput.
- **Queue-based load leveling** uses a queue as a buffer between a task and a service to smooth intermittent heavy loads that could cause the service to fail or time out [10]. Same idea as the asynchronism notes — the queue absorbs rate mismatch.
- **Priority queue** prioritizes requests so higher-priority ones are processed before lower-priority ones, for applications offering different service-level guarantees [11].
- **Claim check** splits a large message into a claim check and a payload — send the (small) claim check to the message bus and store the (large) payload in external storage, protecting the bus and clients from being overwhelmed [12]. Essential when messages carry large binaries.
- **Choreography** has each component participate in the decision-making about a workflow, instead of relying on a central orchestrator [13]. The counterpoint is orchestration (a central scheduler); choreography is more decoupled but harder to follow.
- **Sequential convoy** executes a series of dependent tasks in a specific order, handling errors or failures during execution [14]. Used when order matters — workflows and transactions.
- **Asynchronous request-reply** decouples backend processing from a frontend host, where the backend is asynchronous but the frontend still needs a clear response [15].

## How I use this

The decision is about which layer is the bottleneck. When read shapes diverge from write shapes — heavy read load with many query patterns against one write model — I reach for CQRS with materialized views, and event sourcing if an audit trail or temporal queries are also required. When producers and consumers need decoupling — fan-out, load leveling, parallel workers — I reach for the messaging family: pub/sub for one-to-many, competing consumers for parallelism, claim check for large payloads. The two families compose naturally: an event-sourced write side _publishes events_ via pub/sub, and competing consumers materialize them into read models. The discipline is to introduce these patterns only when the simpler single-model, synchronous design has stopped working — they add real complexity, and adopting them too early is a common over-engineering mistake.

## References

[1] Microsoft, "Data management patterns," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/category/data-management](https://learn.microsoft.com/en-us/azure/architecture/patterns/category/data-management)

[2] Microsoft, "Messaging cloud patterns," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/category/messaging](https://learn.microsoft.com/en-us/azure/architecture/patterns/category/messaging)

[3] Microsoft, "CQRS pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)

[4] Microsoft, "Event Sourcing pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing](https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)

[5] Microsoft, "Materialized View pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/materialized-view](https://learn.microsoft.com/en-us/azure/architecture/patterns/materialized-view)

[6] Microsoft, "Index Table pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/index-table](https://learn.microsoft.com/en-us/azure/architecture/patterns/index-table)

[7] Microsoft, "Sharding pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding](https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding)

[8] Microsoft, "Publisher-Subscriber pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber)

[9] Microsoft, "Competing Consumers pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/competing-consumers](https://learn.microsoft.com/en-us/azure/architecture/patterns/competing-consumers)

[10] Microsoft, "Queue-Based Load Leveling pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/queue-based-load-leveling](https://learn.microsoft.com/en-us/azure/architecture/patterns/queue-based-load-leveling)

[11] Microsoft, "Priority Queue pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/priority-queue](https://learn.microsoft.com/en-us/azure/architecture/patterns/priority-queue)

[12] Microsoft, "Claim Check pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/claim-check](https://learn.microsoft.com/en-us/azure/architecture/patterns/claim-check)

[13] Microsoft, "Choreography pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/choreography](https://learn.microsoft.com/en-us/azure/architecture/patterns/choreography)

[14] Microsoft, "Sequential Convoy pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/sequential-convoy](https://learn.microsoft.com/en-us/azure/architecture/patterns/sequential-convoy)

[15] Microsoft, "Asynchronous Request-Reply pattern," Azure Architecture Center. [Online]. Available: [https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply](https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply)

```quiz
Q: CQRS separates read and write operations primarily to…
- reduce the number of database tables
- let the read and write sides evolve independently and avoid update conflicts at the domain level
correct: 1
explain: CQRS splits commands (writes) from queries (reads) so each side can be optimized and scaled independently, preventing update merge conflicts.

Q: Event sourcing stores ____ rather than just the current state.
- only the latest version of each record
- the full append-only series of actions taken on the data
correct: 1
explain: Event sourcing uses an append-only event log as the system of record. Current state is materialized by replaying the events.

Q: A large message would overwhelm the message bus. Which pattern addresses this?
- claim check
- priority queue
correct: 0
explain: Claim check splits the message into a small claim check (sent on the bus) and the large payload (stored externally). The bus only carries the token.

Q: Competing Consumers is the pattern behind…
- one publisher fanning out to many independent subscribers, each getting a copy
- multiple workers draining one queue in parallel to maximize throughput
correct: 1
explain: Competing consumers process messages from the same channel concurrently, balancing load and increasing throughput. Pub/sub is the one-to-many copy pattern.

Q: Pub/sub differs from a point-to-point queue because…
- in pub/sub, each subscriber gets its own copy of the event; a queue delivers each message to one consumer
- pub/sub is always faster than a queue
correct: 0
explain: Pub/sub fans out: one event, many subscribers, each a copy. A point-to-point queue delivers each message to exactly one consumer (the competing-consumers case).
```
