Routing Slip
Overview
Section titled “Overview”A routing slip is a list of destinations attached to a message that defines the path it should take through the system. The producer composes the itinerary up front — ['discounts', 'tax', 'shipping'] — and the framework dispatches the message to the first queue. After each successful handler the framework strips the current hop and forwards the same message to the next queue, until the list is empty. Topology decisions live with the caller; the consumers only need to do their own job.
When to use
Section titled “When to use”Use a routing slip when the workflow is a linear pipeline whose shape is decided by the caller rather than the consumer. A discount engine should not have to know that tax comes next and shipping after that — those are deployment-time concerns about how you happen to compose the order pipeline today. Hand the consumers a script and let them follow it.
It is also handy when different callers want different paths through the same set of consumers. A premium-order flow can include a fraud-check hop that a standard flow skips, without either consumer growing a feature flag.
When not to use
Section titled “When not to use”If the next hop depends on the content of the message or on a runtime decision a consumer has to make, you want a Content-Based Router or a small process manager — not a routing slip. Skips itineraries are fixed at send time. Likewise, parallel fan-out belongs in Scatter/Gather or Pub/Sub; the slip is strictly sequential.
Example
Section titled “Example”The starter side composes the itinerary and calls bus.route(...):
import { type Message, createBus } from '@serviceconnect/core';import { createRabbitMQTransport } from '@serviceconnect/rabbitmq';
interface ApplyDiscount extends Message { orderId: string;}
const starter = createBus({ transport: createRabbitMQTransport({ url: 'amqp://localhost' }), queue: { name: 'starter' },}).registerMessage<ApplyDiscount>('ApplyDiscount');
await starter.start();await starter.route<ApplyDiscount>( 'ApplyDiscount', { correlationId: 'c-1', orderId: 'o-1' }, ['discounts', 'tax', 'shipping'],);The message visits discounts, then tax, then shipping. Each consumer registers a normal handler for ApplyDiscount; none of them needs to know about the others.
Any consumer (or middleware) can inspect the remaining slip — useful for logging, tracing, or conditional behaviour at the tail of the pipeline:
import { ROUTING_SLIP_HEADER, asMiddleware, parseRoutingSlip } from '@serviceconnect/core';
bus.use( 'beforeConsuming', asMiddleware(async (context, next) => { const raw = context.envelope.headers[ROUTING_SLIP_HEADER]; const remaining = parseRoutingSlip(typeof raw === 'string' ? raw : undefined); context.logger.info('hop visited', { remaining }); await next(); }),);Behind the scenes
Section titled “Behind the scenes”- The slip is a JSON-encoded queue list carried in the
RoutingSlipheader. - Each consumer forwards to the next destination on any success-classified exit; on throw, forwarding is suppressed and the message stops where it is — the throw is retried per the transport’s retry policy (default 3 attempts on RabbitMQ) and only dead-lettered to that queue’s error queue once retries are exhausted.
- Forwarding is not limited to registered message handlers. The slip also advances past a passive hop — a queue where the type is registered but nothing handles it — and on the success branch of a saga or aggregator. Any hop that consumes the message successfully strips the current entry and forwards the remainder, so a routed message is never acked with its itinerary silently dropped.
- The framework removes the current destination after a successful exit, so the header shrinks by one entry per hop.
bus.route(...)is preferred over manually setting the header; the helpersserialiseRoutingSlipandparseRoutingSlipexist for advanced inspection.
See also
Section titled “See also”- Runnable example:
examples/routing-slip/ bus.route- Filters