dialsConstants you can turn without a deploy
Declare a value in code with a default, a type, and a schema. Override it at runtime. Vary it per market, per platform, per anything. Every change attributed, every read cached.
Declare a value in code with a default, a type, and a schema. Override it at runtime. Vary it per market, per platform, per anything. Every change attributed, every read cached.
Every application accumulates values like these:
CHECKOUT_FEE_BPS = 250
FREE_DELIVERY_THRESHOLD = 5_000
SIGNUPS_ENABLED = trueThen someone needs to change a fee today, so the constant becomes a database row with an admin form. Then a second market launches and needs a different fee, so the row grows a country column and a sentinel value for "global". Six months later the app has some values in specialized domain tables, others in an app-wide settings table, and a home-brewed resolver next to each one — every developer solving fallback, validation, and caching again, slightly differently, with no shared answer to "who changed this and what values are safe?"
Dials is all of that, built once:
minimum:, enum:, pattern:, ...), so they're declarative data an admin surface can render.scoped override → global override → code default, always. Delete every override and you are back to exactly what the code says.dimensions: { market: { enum: %w[KE NG BD] } } arms a dial for per-market values; the gem validates every scope against the declaration.Dials.adjust_checkout_fee_bps(..., actor: current_admin) is the only write path, and it appends to a change log you can render as history.# config/initializers/dials.rb
Dials.define do
dial :checkout_fee_bps, default: 250,
type: :integer, minimum: 1, maximum: 10_000, unit: "bps",
dimensions: { market: { enum: %w[KE NG BD] } }
dial :signups_enabled, default: true, type: :boolean,
description: "Global kill switch for signups."
endEach declaration generates the dial's methods — read with the bare dial name, write with adjust_, remove an override with clear_:
Dials.checkout_fee_bps(market: "KE") # => 250 (code default)
Dials.adjust_checkout_fee_bps(120, actor: current_admin, market: "BD")
Dials.checkout_fee_bps(market: "BD") # => 120 (scoped override)
Dials.checkout_fee_bps(market: "KE") # => 250 (still the default)
Dials.clear_checkout_fee_bps(actor: current_admin, market: "BD")
Dials.checkout_fee_bps(market: "BD") # => 250 (back to code)
Dials.changes(key: :checkout_fee_bps)
# => [#<ChangeRecord action="clear" actor_label="keith@..." ...>, ...]The docs stay grounded in Bazario, a fictional commerce platform in three markets (KE, NG, BD) across three platforms (ios, android, web). Bazario is a real Rails app — the demo/ package in this repository — and its spec suite proves every claim these pages make.
Not a secrets manager, not deploy configuration, not a feature-flag system with percentage rollouts, and not for per-market values that are deliberately priced as bundles. The boundary matters; it has its own page: When NOT to Use a Dial.