Rototo Use Cases
The concepts page explains what the pieces are. This page answers the question that comes before that one: what do teams actually put in a runtime configuration package?
The honest answer is that "runtime configuration" is one mechanism doing ten different jobs. A feature flag, a pricing table, a tenant override, and a provider failover feel like different problems, but they all reduce to the same move: the application asks a reviewed package for a value, with runtime facts attached. This page walks the ten jobs. For each one: the production problem, the smallest rototo mechanism that answers it, and a worked example package in the repository you can lint, inspect, and resolve yourself.
Five example packages carry the tour, each a fictional but recognizable SaaS:
examples/release-ops- a team shipping a collaborative editor: flags, rollouts, experiments, kill switches, and operational knobs.examples/billing- an API platform selling tiered plans: entitlements, pricing, and quotas.examples/tenancy-decisioning- a multi-tenant platform whose tenants customize content and decisions within governed limits.examples/regional-policy- a regulated messaging SaaS choosing providers and policies per jurisdiction.examples/environments- one small service across dev, staging, and prod.
Every package is lint-clean and its resolutions are pinned by tests in CI, so what the READMEs claim is what the packages do. Try one:
rototo resolve examples/regional-policy \
--variable sms_marketing_enabled \
--context '{"account": {"id": "a1", "jurisdiction": "eu", "plan_tier": "free"},
"message": {"channel": "sms"}}'
Each package README also names the hard parts it does not solve yet; those map to the roadmap section of the repository README. The point of the examples is trust, and trust comes from stating the boundary, not hiding it.
1. Release control
The problem: shipping code and releasing behavior are welded together. The only way to turn something on is a deploy, and the only way to turn it off is another one. That makes every release an all-or-nothing event, and every incident a race to roll back a binary.
The smallest mechanism is a bool variable. The code path ships dark, gated on
a variable whose default is false. Releasing is a one-line config change,
reviewed and merged like any other. A kill switch is the same variable used in
reverse: a safe default, flipped by a commit during an incident. A ring
release adds rules: employees first, then beta accounts, then
everyone, each ring one rule keyed on an account fact from the
context.
A gradual rollout needs one more piece, because "5% of users" is not a fact the
application can send - it is an assignment rototo must make, and make the same
way every time. That is a layer and an allocation: hash a stable unit like
user.id into buckets, claim a range, and grow the range by editing one line.
The concepts page covers layers; examples/release-ops shows
the whole family - the rollout, the ring release
(variables/enable_realtime_preview.toml), the kill switch
(variables/disable_pdf_export.toml), and a deprecation running in reverse.
2. Experimentation
The problem: an A/B test is a rollout with a stricter contract. Assignment must be deterministic (the same user sees the same arm on every request), the control arm must behave exactly like the baseline, and two experiments touching the same surface must never share a user, or they contaminate each other's measurement.
Layers give you all three structurally. Allocations in one layer claim disjoint buckets, so mutual exclusion is bucket arithmetic a reviewer can check in a diff. Unclaimed buckets are a holdback: users who see pure baseline behavior. And an allocation-driven variable maps arms to values, so "control equals default" is a visible, checkable equality in one file.
In examples/release-ops, layers/editor.toml holds a rollout at 20%, an
experiment at 70%, and a 10% holdback on one line of 100 buckets;
variables/toolbar_layout.toml reads the experiment. One boundary to know up
front: rototo assigns arms and explains the assignment in the trace. Logging
exposures, computing statistics, and picking a winner belong to your
experimentation loop, not to rototo.
3. Operational tuning
The problem: timeouts, batch sizes, retry budgets, and log levels are the values operations actually changes at 3am, and in most systems changing them means a redeploy under pressure.
These are ordinary variables - an int with a default and a couple of rules is
usually the whole story. The value of putting them in a package is the review
and the blast-radius honesty: autosave_interval_ms and log_level in
examples/release-ops show per-condition dials (debug logging for employees,
a gentler autosave for large documents), and examples/environments shows the
same knobs varying per environment.
One caution this page owes you: a changed knob hits 100% of matching traffic on the next refresh. Config changes cause outages at the same rate as code changes, and canarying a value change is still an open roadmap item, so treat a hot-path knob edit with deploy-level care.
4. Plans, entitlements, and pricing
The problem: what each plan includes, what it costs in each currency, and what each account may do tends to be scattered across code constants, a billing system, and tribal knowledge. Money config is where a typo is a revenue incident.
This is catalog territory. Plans, features, and prices are
catalogs - typed tables with JSON Schemas - and the
relationships between them are references, so a plan pointing at a feature
that does not exist is a lint failure, not a production surprise. Selection is
a catalog query: active_price in examples/billing filters
prices to the account's (tier, currency) key and takes the newest entry
whose effective_from is in the past. That last clause is effective dating
with no new machinery: prices are append-only, a change is a new entry
authored ahead of its date, and the October increase in
data/catalogs/prices/team_usd_2026_10.toml takes force at its instant with
no human present.
Query predicates see hydrated views, so selection can filter on referenced
data; active_plan returns the raw plan entry, and application code follows
the feature references it actually needs through the SDK's lookup surface
instead of hard-coding string constants.
5. Tenant customization
The problem: every serious customer wants something different, and the naive answers are both bad. Fork the config per tenant and the forks drift from the base forever; put tenant conditionals in the base and it grows a rule per customer until nobody can review it.
Rototo's answer is composition with governance. The vendor
ships a base package; each tenant is a thin overlay that extends it. The
overlay can add a catalog entry, update fields of one, delete one, or
replace a variable's [resolve] block wholesale - but only where the base's
governance.toml grants it, and grants are enforced at compose time as load
failures, not review conventions. The base's schemas and types stay the
contract: a tenant value that breaks them is a lint failure.
examples/tenancy-decisioning is the canonical pair: a platform base/ and
one tenant's acme-tenant/ overlay that exercises every composition mechanism
exactly once, including the overlay narrowing governance further for the
tenant's own teams.
6. Personalization and decisioning
The problem: which banner, which offer, which algorithm - per-request
decisions that product teams want to change weekly, and that harden into
unmaintainable code when they ship as if chains.
The mechanism is ranked selection: a query that filters candidates and sorts
by priority, so the decision logic is data in one reviewable file. In
examples/tenancy-decisioning, homepage_banner filters the banners catalog
by campaign window and audience, sorts by priority, and returns the top entry.
Audiences there are worth a look: each is a catalog entry holding condition
bounds over visitor facts, referenced from banners by id, so a typo'd audience
is a lint failure and the definition is reused across banners.
The boundary that keeps this healthy: an audience is a condition over context facts, never an enumeration of ids. "These 5,000 visitors" belongs in your database, surfaced as a context fact the condition can test.
7. Compliance and regional policy
The problem: what is legal varies by jurisdiction and changes on a regulator's schedule, not yours. And when the auditor asks what the retention policy was on March 3rd, "someone changed a database row" is not an answer.
The mechanism is deliberately plain: variables with rules keyed on a
jurisdiction fact. examples/regional-policy turns marketing SMS off in the
EU (sms_marketing_enabled), sets data_retention_days per region, and holds
fraud thresholds as reviewed numbers. The part that makes it compliance-grade
is not a feature, it is the medium: the package is a git repository, so the
config in force on any date is a git log question, with the author and the
review attached.
One honest gap, stated in that package's README: today's governance controls who may edit what, but it cannot yet pin an outcome so that no lower layer or experiment can re-enable a jurisdiction gate. That dominance guarantee is an open roadmap item.
8. Provider routing and migrations
The problem: the email vendor is down, the SMS carrier needs to change in one country, and a third of your AI spend rides on which model answers support tickets. Vendor choice changes faster than deploys and needs an audit trail.
The mechanism is a catalog of providers plus a query: filter to the active
providers for this (channel, jurisdiction), sort by priority, take the top.
Failover is then a two-line data edit - mark the primary disabled, or
out-rank it with the backup - reviewed and logged. examples/regional-policy
does exactly this in message_provider, migrates delivery traffic tenant by
tenant through a layer-allocated variable (use_new_delivery_pipeline), and keeps AI
configuration as catalog entries where the model id, prompt version, and
parameters travel together, so a resolution can never mix prompt v14 with the
wrong model.
Secrets stay out, always: config names the provider, the application holds the credential.
9. Time-based change
The problem: a campaign starts Friday at midnight, a rebrand is embargoed until the announcement, a price takes force on October 1st. Someone should not have to be awake for any of those.
The mechanism is env.now, the evaluation timestamp every
expression can read. A campaign window is a catalog entry
with start and end fields and a query filter that tests them - the
spring_sale banner in examples/tenancy-decisioning opens and closes on its
own. A scheduled flip is one rule: enable_v3_branding in
examples/release-ops turns on at an exact UTC instant. Effective-dated
pricing in examples/billing is the same idea aimed at money.
The known sharp edge: resolvers that cache results must know a value changes at the boundary instant. Time-boundary cache awareness is an open roadmap item, so keep refresh periods short around scheduled flips.
10. Environment separation
The problem: dev, staging, and prod should differ in values - bucket names, timeouts, debug endpoints - and in nothing else. In practice each environment accumulates its own config file, and the drift between them becomes a spreadsheet nobody trusts.
The mechanism is the same layering built for tenants, used vertically. In
examples/environments, base/ carries the contract and production values -
prod is the environment that must never be wrong by omission, so it is the
layer with no overlay to forget. dev/ and staging/ extend the base and
carry exactly the files that differ, so environment drift is a short directory
listing. Which environment applies is a deployment-time choice: each
deployment starts with its environment's package
source, so environment never becomes a context fact
that rules can branch on, and the three environments stay structurally
identical.
What rototo leaves to the application
Some hard things nearby are deliberately not rototo's job. Naming them here means you meet a boundary instead of a wall:
- Exposure logging and experiment analysis. Rototo assigns and explains; logging exposures, computing stats, and deciding winners is your experimentation loop.
- Metric-driven automatic rollback. Rototo makes state reviewable and revertable; watching metrics and deciding to revert is your deploy loop.
- Enumerated ID sets as targeting. Conditions over context facts, yes. "These 5,000 tenant ids", no: that set belongs in your database, surfaced as a context fact.
- Secrets. Config names the provider; the application holds the credential.
- Identity resolution. The application supplies a stable unit; rototo hashes it.
- Enforcement and reconciliation. Rototo is the desired-state source that read-only consumers poll; Terraform-style enforcement is a downstream consumer, not a rototo subsystem.
Where to go next
If a section above matched your problem, open its example package and read the README first - each one states what its tests pin and which hard parts are still open. The concepts page defines the vocabulary the examples use, the package format reference specifies every file shape, and Using Rototo covers getting a package reviewed, released, and refreshed in production.