Home

/

The Production-Ready Playbook

/

Messaging & Scale

Messaging & Scale

Chapter 6
Part II
5
min read

The last altitude kept your state safe. This one keeps it moving. The moment the order service hands work to the kitchen, the courier-matcher, or the payment provider over a network, you inherit a new class of problem. The producer is fast and bursty. The consumer is slow and steady. And the connection between them can drop at the worst possible second. The patterns here put a buffer in that gap, scale the slow side out, and turn "I'll process this OrderPlaced later" into a promise you can actually keep. We carry the food-delivery marketplace through them all. A customer places an Order, a restaurant (the tenant) cooks it, a courier delivers it, and at lunch every one of those steps is on fire at once.

A broker sits at the centre of all of it. The book stays neutral on which one. Examples name GCP Pub/Sub first, with AWS (SQS + SNS) and Azure Service Bus as the counterparts; the patterns outlive any of them.

Queue-Based Load Levelling

The lunch rush hits and a fixed-capacity consumer falls over at the peak (Azure Cloud Design Patterns). Noon and 7pm are not the average; they are five minutes of orders arriving at ten times the baseline rate. The fix is to put a queue between the order intake and the work behind it, so customers place orders at their own pace and the kitchen pipeline drains at its own.

// Order intake: accept fast, return immediately.
public async Task<IActionResult> PlaceOrder(Order order)
{
    await _publisher.PublishAsync("orders.placed", order);  // enqueue, don't cook
    return Accepted();                                      // 202 — order is on the queue
}

What this buys you in production: the order pipeline is sized for the average lunch, not the noon spike. A rush that would have taken the synchronous version down instead becomes a queue that grows for a few minutes and drains. Your database connection pool sees a steady trickle instead of a thousand orders landing in the same ten seconds. The endpoint returns in milliseconds because it does almost nothing but accept the order.

The skip-if: if the caller needs the answer now (a card authorisation, a "is this restaurant still open" check), a queue is the wrong tool. Asynchronous processing means the customer's app has to come back later to ask how it went. Don't level load that has to be synchronous.

Levelling the dinner-rush spike

Competing Consumers

One courier-assignment worker can't keep up with the queue, and the backlog of unassigned orders grows without bound (Azure Cloud Design Patterns; Hohpe & Woolf, Enterprise Integration Patterns). Point several identical assignment workers at the same queue and let the broker hand each order to whichever worker is free.

// N identical courier-assignment workers, each pulling from the same subscription.
await foreach (var msg in subscription.ReadAllAsync(ct))
{
    await _matcher.Assign(msg.Order);   // CourierMatchingStrategy picks a courier
    await msg.AckAsync();               // ack only after success — see DLQ below
}

What this buys you in production: horizontal scale with no code change. At lunch you run twenty assignment workers; at 3pm you run two. Throughput is a dial you turn by adding workers, and the broker handles the distribution. When a worker dies mid-assignment, the broker redelivers to a sibling once the lease expires, so a crash costs you a retry, not an order that never finds a courier.

The skip-if: two things have to hold. The work must be parallelisable (no ordering dependency between orders), and each handler must be idempotent, because redelivery means an order can be assigned more than once. If assigning the same order twice books two couriers, fix that before you add the second worker. We come back to idempotency in the resilience chapter; here it's the precondition that makes competing consumers safe.

Adding workers is a dial. Making the assignment safe to run twice is the engineering.

Publish/Subscribe

A queue delivers each message to exactly one consumer, but one placed order is news to four different parts of the business at once (Azure Cloud Design Patterns; EIP Publish-Subscribe Channel). The kitchen has to start cooking, courier-matching has to find a rider, the customer has to see "order confirmed," and analytics has to count it. Publish to a topic instead of a queue, and every subscriber gets its own copy.

The order service stops knowing who its consumers are. It announces OrderPlaced; the kitchen, courier-matching, the customer-notification service, and analytics each subscribe on their own. Adding a fraud-screening consumer later is a new subscription, not a change to the order service. That decoupling is the whole point.

// One publish, fanned out to every subscriber of the topic.
await _bus.PublishAsync(new OrderPlaced(orderId, tenantId, total));
// kitchen, courier-matching, customer notifications, analytics — each its own subscription

What this buys you in production: the sides of the marketplace evolve independently. The team that owns analytics adds a subscription without filing a ticket against the order service. On most brokers a topic with one subscription is just a queue, so you can start point-to-point (order service → kitchen only) and fan out to couriers and analytics later without rebuilding.

The skip-if: pub/sub is for facts that already happened, broadcast to whoever cares. OrderPlaced is a fact. If you instead need a specific worker to assign a specific courier and tell you the result, that's a command on a queue, not an event on a topic. Broadcasting a command to nobody-in-particular is how an order quietly never gets a courier.

OrderPlaced fanned out
the-pareto-stack-cloud-design-patterns-for-small-teams
the-ladder-of-altitudes
how-to-read-this
object-level-the-patterns-that-earn-their-keep
decorator
state
component-level-structuring-one-service
ports-and-adapters-hexagonal
mediator-the-commandquery-split
data-persistence
optimistic-concurrency
messaging-scale
outbox
resilience-staying-up-when-dependencies-dont
rate-limiting-throttling
timeout-fallback
the-composed-pipeline
observability-diagnostics-seeing-inside-production
metrics-the-four-golden-signals
externalised-configuration
hosting-cloud-agnostic-by-default
sidecar-ambassador
orchestrator-agnostic-deploy
a-reference-service
the-relay-outbox-to-queue
the-payment-saga-charge-pay-out-compensate
the-over-engineering-tax
conclusion-production-ready-deliberately
the-pattern-quick-reference-card
altitude-3-data-persistence
altitude-5-resilience
the-skip-list
full-event-sourcing-for-crud
robert-c-martin-uncle-bob-the-house-authority-for-structure
altitude-2-component
altitude-4-messaging-scale
altitude-6-observability-diagnostics

Download the full PDF for free?

Free download — no account required

Get the PDF
Get the PDF
Related Chapters
Free Download
Get the full PDF
All pages, including all code examples, diagrams, and the appendix reference card.
No spam. Unsubscribe at any time.
Your email won't be shared.
Oops! There's a problem with your request. We're working on fixing it. Please try again later.