Operational Switches
Operational switches are the values I reach for when production behavior needs to change quickly, but not casually. During an incident, you may need to stop new project creation while existing projects keep working. That decision should have a diff, review, lint, history, rollback, and a clear answer to "why did this request get blocked?"
That is a good fit for rototo. The switch is not user state, queue state, or an authorization decision. It is reviewed operational policy that the app can refresh and apply at runtime.
I will keep the workspace small: operations-config, with one service boundary
that checks whether project creation is enabled.
Start With A Broad Switch
The first version does not need runtime context. Either project creation is enabled for everyone, or it is disabled for everyone.
Create a workspace with one variable:
rototo init operations-config --variable project-creation-enabled
Replace operations-config/variables/project-creation-enabled.toml:
schema_version = 1
description = "Whether accounts can create new projects"
type = "bool"
[values]
enabled = true
disabled = false
[resolve]
default = "enabled"
The variable has two named values, but only one is selected. That naming matters
more than it may seem at first. In logs, traces, and reviews, disabled says
more than a bare false.
Lint and resolve the switch:
rototo lint operations-config
rototo resolve operations-config --variable project-creation-enabled
Because this switch has no rules yet, rototo evaluates it with {} context and
selects the default value:
variable: project-creation-enabled
pathway:
default -> enabled
result:
value key: enabled
value: true
Check The Switch In The App
The app should ask rototo for the switch at the boundary where it matters. In this case, that is the project creation path. The app still owns authorization, request validation, and database writes; rototo only answers the operational policy question.
use rototo::{ResolveContext, Workspace};
async fn create_project(workspace: &Workspace) -> Result<(), Box<dyn std::error::Error>> {
let context = ResolveContext::from_json(serde_json::json!({}))?;
let resolution = workspace
.resolve_variable("project-creation-enabled", &context)
.await?;
let value_key = resolution.value_key.clone();
let creation_enabled: bool = serde_json::from_value(resolution.value)?;
if !creation_enabled {
println!(
"project creation blocked by rototo value `{}` from {:?}",
value_key,
workspace.source_fingerprint()
);
return Ok(());
}
// Validate the request, authorize the account, and create the project.
Ok(())
}
I like this placement because the app code stays honest about the boundary. Rototo is not deciding who the user is or whether they have permission. It is deciding whether this reviewed operational path is open right now.
Disable Through Review
If you need a global pause, change the selected default:
[resolve]
default = "disabled"
Run lint, open the smallest PR that explains the change, and merge it through the same path as a code change:
rototo lint operations-config
git add operations-config
git commit -m "Disable project creation during incident"
Long-running services that use RefreshingWorkspace keep serving the last
successfully loaded workspace until a refresh succeeds. After the merge reaches
the workspace source, a successful refresh affects future project creation
checks. If a refresh fails, the service keeps the last known-good workspace
active.
Rollback follows the same path: revert the change, or make a new reviewed change
that sets the default back to enabled.
Scope The Switch
A global switch helps when the whole system is affected. More often, the
incident has a boundary: one region, one account class, or one integration. I
prefer adding that boundary to the workspace instead of scattering if checks
through app code.
Restore the default to enabled, then create
operations-config/qualifiers/eu-accounts.toml:
schema_version = 1
description = "Accounts operating in the European region"
[[predicate]]
attribute = "account.region"
op = "eq"
value = "eu"
Now update operations-config/variables/project-creation-enabled.toml so only
that named condition selects the disabled value:
schema_version = 1
description = "Whether accounts can create new projects"
type = "bool"
[values]
enabled = true
disabled = false
[resolve]
default = "enabled"
[[resolve.rule]]
qualifier = "eu-accounts"
value = "disabled"
The rule says exactly what the incident response means: disable project creation for accounts in the European region; keep the default open.
Generate The Context Contract
The qualifier now reads account.region. That is the right moment to generate
the context schema skeleton from the workspace:
rototo init operations-config --context
On this workspace, rototo writes
operations-config/schemas/context.schema.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": true,
"properties": {
"account": {
"type": "object",
"additionalProperties": true,
"properties": {
"region": { "type": "string" }
}
}
}
}
The schema makes the app contract explicit. If the workspace reads
account.region, the app must send context with that shape when it resolves
the switch.
Lint the workspace:
rototo lint operations-config
Then resolve both paths:
rototo resolve operations-config \
--variable project-creation-enabled \
--context account.region=us
value key: enabled
value: true
rototo resolve operations-config \
--variable project-creation-enabled \
--context account.region=eu
value key: disabled
value: false
Now the app supplies a fact it already knows at the request boundary, and the workspace owns the reviewed decision about what that fact means operationally.
Keep The Boundary Clear
Operational switches fit rototo when the value should be reviewed, refreshed, and explainable. They do not fit every kind of runtime decision.
Reach for this pattern when the runtime decision is:
- incident controls;
- temporary operational pauses;
- region or account-class policy;
- kill switches for risky workflows;
- reviewed behavior changes that should not require an app redeploy.
Keep these decisions somewhere else:
- per-request authorization;
- user preferences;
- counters and quotas that update on every request;
- queue state or workflow state;
- high-volume mutable data.
That is the split worth protecting. Rototo gives the app a reviewed operational answer. The app still owns identity, permissions, state changes, and the domain logic around the request.