Back to blog
Article

Event Routing Patterns: Per-Event-Type, Per-Customer, Per-Tier

By Aylon··5 min read

Once you have multiple destinations, routing decides which events go where. The patterns that work and the ones that fall apart at scale.

Routing is the layer between ingest and delivery that decides which events go to which destinations. With one destination, routing is trivial, everything goes there. With ten, it becomes the most-touched configuration in the system. The patterns that work at scale look different from the naive version.

Pattern 1: per-event-type routing

The simplest rule: this destination only gets events of these types.

Destination: customer-acme-webhook
  Event types: order.completed, order.refunded, subscription.renewed

This pattern works for the majority of cases. The customer wants notifications for specific business events; they configure their destination to receive those types only.

Implementation: a string-set filter applied per delivery. Cheap, fast, and the most common rule.

The thing to get right: the event-type taxonomy. If your event types are too granular (order.completed.us, order.completed.eu), the customer ends up listing dozens. If too coarse (order.event), they over-receive. Aim for the noun.verb pattern (order.completed, subscription.renewed) and let payload fields carry the regional detail.

Pattern 2: per-customer routing

When you have multi-tenancy, every destination is implicitly scoped to one customer. The routing decision is "did this event belong to this customer?"

Destination: bigquery-acme-prod (owned by customer "acme")
  Routes events where customerExternalId == "acme"

This pattern is enforced by the platform, not by user-configurable rules. A destination belongs to a customer; events for other customers can never route to it. The customer's data team can't accidentally receive another tenant's events because the platform forbids it.

This is the foundation of tenant-scoped delivery.

Pattern 3: per-tier routing

Some events should only go to higher-tier customers (PII-scrubbed, full-fidelity, real-time vs delayed). The routing rule references the customer's tier:

Destination: scrubbed-events-bucket
  Routes events where tier in ("free", "base")
  Apply transform: scrub_pii

Tier routing pairs naturally with transforms, events of certain types only go to free-tier customers with sensitive fields removed, while enterprise customers get the full shape. The decision lives in routing; the actual scrubbing lives in transforms.

Pattern 4: payload field filtering

Sometimes you want events but only when a field matches:

Destination: high-value-orders-webhook
  Event types: order.completed
  Filter: payload.amount > 10000

This pattern is more advanced but very common in practice. The customer wants notifications only for material events, large orders, high-priority incidents, escalation-class signals.

Implementation: a JSON-path expression evaluated per delivery. The expressions should be limited to safe operations (comparisons, equality, simple boolean combinations), don't expose a full scripting environment in routing.

Pattern 5: routing for fan-out

The same event can match multiple destinations. The router produces one delivery job per match:

order.completed event arrives
  → matches "customer-acme-webhook" (event type filter)
  → matches "warehouse-acme-bigquery" (event type filter)
  → matches "high-value-orders-webhook" (payload filter, if amount > 10000)
  → creates three delivery jobs

Each delivery is independent. One destination failing doesn't affect the others. The customer can replay one destination's failures without replaying the others.

What doesn't work

Patterns that look reasonable but fall apart at scale:

  • Global event routing. "All events of type X go to all destinations of type Y." Eliminates per-customer scoping and makes tenant isolation impossible. Don't do it.

  • Chained transforms in routing. Routing decides where; transforms decide what. Mixing them ("if destination is X, apply transform Y") creates implicit coupling that's hard to reason about. Keep them separate: routing is a filter, transforms are a function.

  • Stateful routing. "Send every 10th event to the sampling destination." Stateful routing creates correctness problems on replay (the counter is wrong) and on partition rebalance (each shard counts independently). Stick to stateless filters.

  • User-defined scripting in filters. Tempting but a security risk and a debug nightmare. Keep filters to declarative expressions; if customers need scripting, that's a transform concern, not a routing concern.

What the dashboard should expose

For each destination, the customer should see:

  • The rules that route events to it.
  • A preview of recent events that matched.
  • A preview of recent events that didn't match (with the rule that excluded them).
  • The ability to test a rule against a sample event before saving.

That last item, "test before save", is what makes routing usable in practice. Customers iterate on rules; rejecting events silently because a typo in the filter is a support-ticket generator.

How Pushrail does it

Pushrail exposes routing rules as per-destination filters covering event-type, payload fields, and per-customer scoping. The rule evaluator runs per delivery decision, supports the patterns above, and exposes preview + test before save. Tenant scoping is enforced architecturally, the platform doesn't let a destination match another tenant's events.

Next in the Beyond-webhooks cluster: customer integration portals, what self-serve destination management looks like.

Ready to stop building delivery infrastructure?

Start free. Send your first event in under 5 minutes.

Protected by reCAPTCHA, Google's Privacy Policy and Terms apply.