Routing
Edit this pageThe mark of a healthy message architecture: producers don’t know consumers exist. A producer states what happened, tagged with a routing key; each consumer group declares which keys it cares about. In SQLStreams that declaration is a binding — a stored pattern matched against the message’s routing key when the group’s deliveries materialize.
Produce with a routing key
produced, err := instance.Produce(ctx, &OrderCreated{OrderId: order.Id},
&sqlstreams.ProduceOptions{RoutingKey: "orders.eu.created"})
The key is optional. A message produced without one is received only by groups that declared no bindings — it carries nothing for a pattern to match.
Declare the group’s bindings
A group’s bindings are declared as a whole set, in ConsumerConfig.Bindings
at Register — there is no separate bind verb to call one pattern at a time:
orders := client.Stream[OrderCreated](registered.Name)
// Receipts care about created orders.
receipts, err := orders.Consumer("email-receipts").Register(ctx,
&sqlstreams.ConsumerConfig{Bindings: []string{"orders.*.created"}})
// EU compliance wants everything EU.
compliance, err := orders.Consumer("eu-compliance").Register(ctx,
&sqlstreams.ConsumerConfig{Bindings: []string{"orders.eu.*"}})
// The analytics group passes nil: no bindings, the whole stream.
analytics, err := orders.Consumer("order-analytics").Register(ctx, nil)
Produce orders.eu.created → email-receipts, eu-compliance, and
order-analytics all receive it; a us-warehouse group bound to
orders.us.* hears nothing. The producer changes zero lines when a new
group comes online.
The pattern language: one wildcard, honestly scoped
* matches any run of characters — including dots. The pattern is
anchored at both ends; everything else is literal:
| Pattern | orders.eu.created | orders.us.central1.created | orders.eu.updated |
|---|---|---|---|
orders.*.created | matches | matches — * crosses depth | no |
orders.eu.* | matches | no | matches |
orders.* | matches | matches | matches |
Note the second column: * is a true wildcard, so a pattern cannot pin
an exact segment depth (“exactly one segment here”). If you need to keep
hierarchies apart, make the literal parts distinctive. A NATS-style
one-segment wildcard is a possible future refinement, not a shipped
behavior.
How a set change lands
Bindings are per-group state that many deployed instances declare, so declaration has explicit outcomes rather than last-writer-wins:
- installed — the declared set is now the group’s effective set (the first declarer, or nothing live disagrees).
- joined — the declared set was already stored; nothing to change.
- waiting — a live instance still declares a different set. The
waiting instance retries (every
BindingRetryInterval, default10s) until the disagreeing instances go away — a deploy rolling out a new binding set converges as the old instances stop.
Inspect where every group stands:
sqlstreams system binding list
One group’s effective set is a read on its handle. nil means the group never declared a set and receives the whole stream:
binding, err := client.Stream[OrderPlaced]("orders").Consumer("email-receipts").Binding().Get(ctx)
if err != nil {
return err
}
if binding != nil {
fmt.Println(binding.Status, binding.Patterns)
}
The same read from the shell is sqlstreams consumer binding get orders email-receipts.
It’s all rows
Each stream owns a physical binding_config table, named by the stream’s id
(table design). The
pattern column keeps the pattern as you wrote it; the POSIX
translation it matches with sits beside it as pattern_regex:
SELECT g.name AS consumer_group, b.pattern
FROM sqlstreams.binding_config_1 b JOIN sqlstreams.consumer_group_config g ON g.id = b.consumer_group_id
ORDER BY 1;
Every declaration attempt — installed and waiting alike — is also
appended to the stream’s binding_config_log table with who declared it and
when, so “who changed the routing and when” is a query, not a mystery.
What routing is not (yet)
Matching is on the routing key only. Header or content matching
(“region=eu AND tier=gold” over a JSONB attribute set) is a designed
follow-up that waits for a real workload — today, encode what you route
on into the key.