Routing Strategies

Learning

Multi-armed and contextual bandits — Thompson sampling and LinUCB — that learn which recipient wins from outcomes.

Learning strategies don't score a fixed blend of signals — they learn which recipient wins by trying candidates and observing outcomes. They balance exploration (trying uncertain recipients to gather data) against exploitation (routing to known winners). They derive their state from durable outcome-learning evidence, and all of them live in the optional predicted contrib module.

StrategyTypeCapabilitiesAliases
Thompson samplingBayesian banditexplain, statefulbandit
LinUCBContextual banditexplain, stateful

RegisterPredicted (modules/strategies/predicted/register.go) registers predicted_value, thompson_sampling, linucb, and the bandit alias for thompson_sampling; the factories themselves live in pkg/strategy/builtin/predicted/.

Module-gated: enable it and promote a deployment

These strategies are not in the built-in pack. To use them the predicted contrib module must be enabled and backed by a promoted deployment; until then any recipe that references them reports missing_dependencies. They require the outcome and quality repositories, and — because their learned posteriors must survive restarts — a BanditStateStore.

Learned state lives in the state plane, not the snapshot

A bandit's posteriors are mutable strategy state, not decision inputs. They live in the routing-managed strategy state plane (Redis-backed via NewRedisBanditState for the fast path, Postgres as authority) — the same plane that holds SWRR weights and fair-catchup windows. That is a different plane from the read-only feature snapshot they read as context. Selection reads a state snapshot and emits a post-commit mutation intent; it never writes state inside Select.

Thompson sampling

Capabilities: explain, stateful · Aliases: bandit

A classic multi-armed bandit. It treats each recipient as an "arm" with a posterior distribution over its reward, samples from each posterior, and routes to the arm with the highest sample. Arms it is uncertain about have wide posteriors and therefore occasionally win the sample — that is exploration, and it is automatic rather than a fixed epsilon.

Why use it. When you don't know in advance which recipient is best and you want the system to find out with minimal regret. Thompson sampling is famously sample-efficient: it converges on winners quickly while still probing the field, and it self-corrects when a recipient's performance drifts. Ideal for lead routing, offer selection, and any assignment where "best" is discovered, not declared.

How to configure it. No product-surface sliders — behavior comes from the per-arm posteriors it maintains. It is stateful: posteriors are updated from committed outcomes and persisted through the bandit state store. It advertises explain, so it can report the sampled values behind a decision.

Where it fits. The Select stage. Because it is deliberately non-deterministic (its determinism profile is exploratory bandit), its strategy contract requires a learning-dataset governance requirement — the platform will not certify an exploratory strategy without one.

LinUCB

Capabilities: explain, stateful

A contextual bandit. Where Thompson sampling learns one reward distribution per recipient, LinUCB learns a linear model per arm over the routable's features, plus a confidence bound. It answers a sharper question: not "who is best overall?" but "who is best for this kind of routable?"

Why use it. When the right recipient depends on the request. A recipient who excels at high-value enterprise leads may be mediocre on small self-serve ones; LinUCB learns those conditional patterns from features and routes accordingly, using its upper confidence bound to keep exploring arms it hasn't seen enough of under the current context.

How to configure it. No product-surface sliders. It consumes routable features and maintains per-arm model state (also stateful, also durable through the state store). It advertises explain.

Where it fits. The Select stage. It reads the typed feature snapshot as its context vector, which is also what makes its decisions replayable for audit and offline evaluation.

Behavior change: contextual exploration now uses features

LinUCB's exploration step now receives each candidate's own feature vector as its context. Previously exploration was handed an empty vector, so the contextual model could not influence which arm was explored and selection silently degraded to uniform random. With per-candidate features flowing through, LinUCB's confidence bounds now actually steer exploration — expect its exploratory picks to reflect the routable's features rather than being effectively random.

Choosing between them

Rule of thumb

Use Thompson sampling when a recipient's quality is roughly the same across all requests — you just need to find the best. Use LinUCB when quality depends on the request's features and you want per-context winners. Both need enough traffic to learn; on a cold or tiny pool, a scoring strategy with a confidence gate is safer.