Routing, Fanout, and Subscriber Independence

LESSON

Event-Driven Architecture and Streaming Foundations

006 30 min intermediate

Routing, Fanout, and Subscriber Independence

By the end of this lesson, you will be able to...

  • Trace how one event is routed and fanned out to independent subscribers.

  • Explain where subscriber independence improves a system and where routing still couples services.

  • Review a routing rule by naming its owner, failure signal, and business meaning.

Idea in one sentence: Routing and fanout let one published fact reach several independent subscribers, but the routing rules themselves become part of the system contract.

Core Insight

The order service publishes one event:

OrderPlaced(order_id=O-812, region=EU, total=120)

Three systems care:

The order service should not call all three systems directly. That would put downstream knowledge back inside the producer.

The tempting answer is:

Publish the event once and let the broker handle it.

That is close, but incomplete.

The broker does not magically know who should receive the event. Some routing rule must decide which subscription receives which message. Some fanout mechanism must create independent delivery paths. Each subscriber then needs its own progress, retry behavior, and failure signal.

The core mechanism is:

producer event -> routing rule -> matching subscriptions -> per-subscriber delivery state

That mechanism creates independence because email can fail without stopping analytics, and analytics can lag without forcing fraud to lag.

But it also creates a new design responsibility. If the routing rule contains business meaning, someone must own that meaning. Otherwise the system is only decoupled in code. The coupling has moved into broker configuration.

The Situation

Continue the topology from the previous lesson.

The order service owns the fact OrderPlaced. It publishes the fact to an orders.placed route or topic.

The consumers are independent:

Subscriber Needs Tolerates lag? Side effect
Email Every placed order. A few minutes. Sends confirmation email.
Analytics Every placed order. Hours, if reports have a freshness target. Updates derived tables.
Fraud Orders above a risk threshold or from selected regions. Seconds to minutes. Scores order and may request manual review.

The producer should not know all of this.

The producer should publish the fact it owns:

{
  "type": "OrderPlaced",
  "event_id": "evt-1001",
  "order_id": "O-812",
  "customer_id": "C-44",
  "region": "EU",
  "total": 120,
  "occurred_at": "2026-07-08T10:15:00Z"
}

The routing layer then decides which subscribers receive a copy.

This is where routing and fanout become visible.

The Moving Parts

Plain meaning:

Routing is the decision about where a message should go.

In our scenario:

OrderPlaced goes to the email subscription, the analytics subscription, and maybe the fraud subscription.

Technical name:

A routing rule maps message metadata, topic names, routing keys, headers, or event content to one or more delivery paths.

Plain meaning:

Fanout means one published message creates several delivery attempts.

In our scenario:

One OrderPlaced event creates one delivery path for email, one for analytics, and one for fraud if the fraud rule matches.

Technical name:

Fanout is one-to-many delivery from a producer-facing route to multiple subscribers or consumer groups.

Plain meaning:

Subscriber independence means each subscriber can make progress, retry, lag, or fail without forcing the others to share the same state.

In our scenario:

If email's SMTP provider is slow, analytics can still consume the event and fraud can still score it.

Technical name:

Each subscriber has separate delivery state: acknowledgment state, retry state, backlog, consumer offset, or dead-letter path, depending on the topology.

These pieces are separate:

Publication:       Order service records and publishes OrderPlaced.
Routing:           Broker or routing layer matches the event to subscriptions.
Fanout:            The event is copied or made visible to each matching subscriber.
Subscriber state:  Each subscriber tracks its own delivery progress and failures.

Keeping those pieces separate is the main design skill.

The Naive Idea

The team starts with a simple rule:

All OrderPlaced events go to all consumers.

At first this feels safe. Nobody misses anything.

But a few problems appear.

Fraud receives low-risk orders it does not need. It spends money scoring harmless cases.

Analytics receives every event, which is correct, but it falls behind during a report rebuild.

Email has a provider outage. Its retry queue grows.

Now the team wants to change routing:

Send EU orders above 100 to fraud.
Send every order to email.
Send every order to analytics.

This is better for fraud cost, but it raises a design question:

Who owns the meaning of "EU orders above 100"?

If this rule lives only in broker configuration, the fraud policy is now hidden. It may not be reviewed with application code. It may not be versioned with the fraud service. It may not appear in the order service contract. A routing rule has become business logic.

The naive idea breaks because it treats routing as plumbing.

Routing is partly plumbing, but it can also be policy.

The Mechanism Step by Step

Here is the mechanism for one event.

Starting state:

Subscriptions:

email-confirmations:
  match: type == OrderPlaced
  state: last delivered event_id = evt-1000

analytics-orders:
  match: type == OrderPlaced
  state: offset = 4096

fraud-screening:
  match: type == OrderPlaced AND region in [EU, US] AND total >= 100
  state: last delivered event_id = evt-0994

Input:

OrderPlaced(event_id=evt-1001, order_id=O-812, region=EU, total=120)

Transition:

The routing layer evaluates the event against each subscription rule.

Intermediate state:

Subscription Rule result Delivery state created
email-confirmations Match. It wants every OrderPlaced. Email receives its own delivery attempt for evt-1001.
analytics-orders Match. It wants every OrderPlaced. Analytics can read evt-1001 at its own position.
fraud-screening Match. EU and total >= 100. Fraud receives its own delivery attempt for evt-1001.

Output:

Order service publishes once.
Email sees evt-1001.
Analytics sees evt-1001.
Fraud sees evt-1001.
Each subscriber has separate progress and retry state.

Now change one field:

OrderPlaced(event_id=evt-1002, order_id=O-813, region=EU, total=40)

Intermediate state:

Subscription Rule result Delivery state created
email-confirmations Match. Email receives evt-1002.
analytics-orders Match. Analytics receives or can read evt-1002.
fraud-screening No match. Total is below 100. Fraud receives nothing for this event.

Output:

Order service still publishes once.
Email and analytics still receive the fact.
Fraud does not receive this event.

Naive failure contrast:

If the order service directly called fraud only when total >= 100, the order service would own fraud policy. If the broker rule owns that policy with no application owner, the policy becomes hidden. If every subscriber receives everything, cost and noise may grow.

The useful boundary is:

The producer owns the fact.
The subscriber owns whether it cares.
The routing rule must have an explicit owner when it contains business meaning.

What Independence Really Means

Subscriber independence is not the same as zero coupling.

It means each subscriber gets its own delivery path and can manage its own progress.

For example:

10:15  Order service publishes evt-1001.
10:15  Email receives evt-1001, but SMTP is unavailable.
10:15  Analytics reads evt-1001 and updates a derived table.
10:16  Fraud receives evt-1001 and marks the order for review.
10:25  Email retries evt-1001 and sends confirmation.

Email being late did not block analytics or fraud. That is real independence.

But the subscribers are still coupled to the event contract:

So the better sentence is:

Fanout reduces runtime coordination between subscribers, but it does not remove contract coupling.

That distinction matters. A team that says "events remove coupling" may stop reviewing schemas, routing keys, and subscriber lag. A team that says "events change the shape of coupling" keeps those responsibilities visible.

Check: The email provider is down for 20 minutes. Analytics continues consuming OrderPlaced events. What part of the design made that possible?

Think first, then reveal.

Answer: Email and analytics have independent subscriber state. Fanout gave each subscriber its own delivery path, so email retries and backlog did not force analytics to share the same failure.

Routing Rules as Contracts

Routing rules have different levels of meaning.

Some rules are mostly technical:

type == OrderPlaced

This rule says, "Send this event type to this subscriber." It depends on the event contract, but it does not contain much business policy.

Some rules are semantic:

region == EU AND total >= 100

This rule says, "Fraud should inspect these orders." That is not just transport. It is a business decision.

Some rules are dangerous because they look technical but hide policy:

routing_key starts with priority.

Who decides priority? What happens when the definition changes? Does the event producer compute it? Does fraud compute it? Does the broker compute it from headers? If nobody can answer, the routing rule has become an unowned contract.

A simple review table helps.

Rule Good owner Reason
type == OrderPlaced Event platform or subscriber configuration. It is mostly subscription shape.
region == EU Subscriber or shared contract, depending on use. It uses stable event data, but still affects who sees data.
total >= 100 Fraud policy owner. It decides business relevance.
customer_tier == premium Owner of customer-tier definition. The meaning may change outside the broker.
schema_version >= 3 Event contract owner. It affects compatibility and rollout.

The safest pattern is not "never route on business fields." That would be too rigid.

The safer pattern is:

If routing changes business behavior, review it like application behavior.

Put the rule where the owner can test it, version it, and explain it. Sometimes that means a broker filter. Sometimes that means a small routing service. Sometimes that means broad delivery to a subscriber that applies its own policy.

Cost, Limits, and Signals

Routing and fanout improve independence.

They let the producer publish once. They let subscribers join without changing producer code. They let each subscriber retry, lag, scale, or fail separately.

They cost operational state.

Every independent subscriber creates something to observe:

They can still fail.

A routing rule can drop messages that should have matched. A broad rule can send too many messages and overload a subscriber. A schema change can remove or rename a field used by routing. A subscriber can be independent but still unsafe if it performs non-idempotent side effects during retries.

This lesson does not solve ordering, partitioning, or idempotency. The next lesson takes those boundaries directly. Here the narrower point is that routing and fanout decide who gets a chance to process the event.

Signals to watch:

Signal What it may mean
One subscriber has growing lag while others are healthy. Subscriber independence is working, but that subscriber has a capacity or dependency problem.
Events are routed to no subscription. A route is missing, a rule changed, or the event type is not recognized.
Fraud receives far more events after a rule change. A semantic rule changed cost or business behavior.
A subscriber dead-letter queue grows after a schema change. The subscriber may depend on fields or meanings that changed.
Producer code changes every time a subscriber is added. Routing is not giving real producer-subscriber independence.

This trade-off is direct:

More targeted routing reduces noise and cost.
It also makes routing rules more important to review.

Common Confusions

Confusion: "Fanout means every process receives every event"

Why it is tempting:

The word fanout sounds like broadcasting to everything.

Better model:

Fanout should create one delivery path per independent subscriber. Inside one subscriber, a worker pool may still share work so only one worker handles one event.

Confusion: "A routing rule is just infrastructure"

Why it is tempting:

Rules often live in broker configuration, Terraform, or an admin console.

Better model:

A routing rule is infrastructure when it only describes transport shape. It becomes application policy when it decides which business cases a subscriber sees.

Confusion: "Subscriber independence means no shared contract"

Why it is tempting:

The producer no longer calls subscribers directly, so the services feel decoupled.

Better model:

Subscribers are still coupled to event names, schemas, meanings, routing metadata, retention, and delivery behavior. The coupling is asynchronous and contractual, not a direct function call.

Check Your Understanding

Check: A new tax service needs every OrderPlaced event for orders shipped to countries where tax reporting is required. Should the order service call the tax service directly?

Think first, then reveal.

Answer: Usually no. The order service should publish the owned fact. The tax service should subscribe through a routing rule or receive broad OrderPlaced events and apply its own policy. If the routing rule contains tax policy, the tax policy owner must own and review that rule.

Check: A broker filter sends only total >= 100 orders to fraud. The fraud team changes its threshold to 80 in code but forgets to update the filter. What failure should you expect?

Think first, then reveal.

Answer: Orders from 80 to 99 may never reach fraud, so fraud code cannot apply its new policy. The routing rule and the subscriber policy disagree. This is hidden semantic coupling.

Practice

Review this proposed routing design:

Product service publishes ProductPriceChanged.

Subscribers:
- Search needs all price changes for searchable products.
- Recommendation wants price changes for products with high traffic.
- Audit needs every price change.
- Cache invalidation needs every public product price change.

Current routing:
- product.price.* goes to search.
- product.price.high_traffic goes to recommendation.
- product.price.public goes to cache.
- product.price.changed goes to audit.

Find two places where routing may hide business meaning.

Then propose a safer ownership boundary.

A strong answer should mention:

Model answer:

high_traffic is business meaning. Recommendation or a traffic-classification owner should own that definition. If the broker route decides high traffic from a routing key, that rule needs tests, versioning, and review by the owner. public is also business meaning. The product catalog owner may own whether a product is public, and cache invalidation depends on that meaning being correct.

Audit probably needs every ProductPriceChanged fact through a broad retained path, not only selected routing keys. Search may need all searchable product changes, but "searchable" is also a product/catalog policy. Useful signals include route match counts, events routed to no subscription, subscriber lag, and sudden changes in match rate after a rule deployment.

Resources

Key Takeaways

PREVIOUS Queues, Topics, Pub/Sub, and Log Topologies NEXT Ordering, Partitioning, and Idempotency Boundaries