Multi-Tenant Feature Flag System Design: From Conditional to Platform
A feature flag platform looks like a glorified boolean in a database at first glance. It isn't. What it actually has to guarantee is that evaluation stays local and fast enough to sit on every request's hot path, while configuration stays centrally governed, versioned, and safely bounded in staleness — without ever leaking one tenant's rules into another's. This module builds one step by step: a hard-coded conditional, a naive central database, local SDK evaluation, tenant namespaces, deterministic percentage rollouts, immutable versioned publication, and safe behavior through control-plane outages and regional kill switches.
Problem Definition
Northstar Cloud (fictional, for this module) ships one application to roughly 200 business customers — tenants. A new collaborative dashboard is ready. Tenant Atlas wants it on for everyone today. Tenant Birch wants a careful ten percent pilot, with beta testers always included. Tenant Cedar wants it off in production until a compliance review finishes, but on in their own staging environment.
This is not primarily a storage problem, even though it looks like one. A flags table with a boolean column already solves 'remember a value per tenant.' What it doesn't solve is: how does a value change without a redeploy, how does one tenant's targeting rule stay invisible to another tenant, how does a rollout percentage stay stable for the same user across requests and across app instances, and what happens to every request currently relying on this system when its central store is unreachable.
Visual Design Journey
Watch a feature flag platform evolve from a single hard-coded conditional into a governed, multi-tenant system — local SDK evaluation, deterministic percentage rollouts, immutable versioned publication, and safe behavior through control-plane outages and regional kill switches — each piece earned by a concrete incident, not added by assumption.
Requirements
Scope this explicitly before any diagram — the scope decisions are half the design.
Functional (V1)
Create, edit, and target boolean and multivariate (string/number/JSON) flags, scoped to tenant, project, and environment. Targeting by explicit allow/deny, by segment membership, and by deterministic percentage rollout. Evaluation from backend services (full rule access) and from browsers (pre-evaluated values only). Every production change requires a review/approval step and is recorded in an audit trail. Administration and bulk analytics endpoints are explicitly out of scope for V1 evaluation-path design — this module focuses on the create → govern → distribute → evaluate pipeline, not a full admin-console feature set.
Non-functional
Local, fast evaluation
Evaluation must be an in-memory function with no network call on the hot path, with a sub-millisecond target per call — the only way thousands of service instances can each check hundreds of flags per request without flags becoming the bottleneck.
Tenant isolation
One tenant's configuration, targeting rules, and audit history must never be visible to or affected by another tenant — enforced at every layer a config value passes through, not just the first authorization check.
Stable, deterministic bucketing
The same user, under the same published configuration, must get the same rollout outcome on every request and on every app instance — never a per-request coin flip.
Bounded staleness, not instant propagation
A connected, healthy SDK should pick up a published change within a documented, measured window. A disconnected or offline SDK has no such guarantee, and the design must say so honestly rather than imply an instantaneous global kill switch.
Availability despite control-plane outages
An already-warmed SDK instance must keep evaluating from its last valid snapshot even if the entire control plane — authentication, approval, publish pipeline — is unreachable.
Flags are not authorization
A flag can decide what a user sees. It must never be the only thing standing between a user and a protected operation — that still needs an independent, real permission check.
Illustrative scale
For reasoning purposes only: roughly 20,000 service instances, each evaluating on the order of 500 flags per second, for roughly 10 million local evaluations per second fleet-wide — compared to a far smaller number of config-distribution requests per second against the central service. These are planning numbers, not measured production traffic; treat every number in this module the same way.
V0 — A Conditional in Code
Start with the simplest thing that could possibly work: if (newDashboardEnabled), where the value is a build-time environment variable.
This is not a strawman. For a single-tenant internal tool, a compile-time constant is a completely legitimate V1. A feature flag platform earns its complexity later, by solving demonstrated problems — not by assumption.
The cracks appear the moment a second tenant wants different behavior. Turning the feature on for Atlas alone still means changing the environment variable and redeploying the entire fleet — there is no 'just for one tenant' at this layer, and every release becomes an all-or-nothing event for every tenant simultaneously.
A Central Flags Database — and Its Limits
Replace the compile-time constant with a flags table and a simple admin API. Each request resolves tenant plus environment plus flag key against the database. At low volume, this is a legitimate, shippable V1.
As usage grows, every request now pays a network round trip and a database query just to answer 'is this flag on' — a cost paid on the hot path of every single request, competing for the same database connections and I/O as the actual application logic. Flag-check volume is now a direct multiplier on database load rather than a near-free local operation.
Then the flags database has an outage — maintenance, connection-pool exhaustion, doesn't matter why. Every request that depends on a flag check now fails too, because the check is synchronous and sits in the critical path. A component whose entire job is to make releases safer has just become the reason the whole application is down.
Local Evaluation via an SDK
Give the application a backend SDK. On startup it loads a snapshot of relevant flags into memory; after that, every evaluation is a pure, local, in-memory function call.
Snapshot cache
The SDK holds a versioned, immutable snapshot of this tenant/project/environment's flag configuration in memory. Evaluation reads only from this local snapshot — never a live network call.
Resilience trade-off, named honestly
Local evaluation trades 'always current' for 'always available.' If the central database is briefly unreachable, an already-warmed instance keeps evaluating from its last loaded snapshot — but a snapshot can go stale for a bounded window between a central change and this instance's next refresh.
Tenant Isolation: Namespace & Authorization
Introduce a real namespace: tenant → project → environment. Every flag definition, override, and published snapshot is scoped to this exact tuple.
Verified credential, not a client-supplied header
Tenant identity for both administration and evaluation comes from a verified, server-side credential — never a caller-supplied tenant ID. A credential scoped to Tenant Birch requesting Tenant Cedar's configuration is rejected with a 403 at the control plane, before any data is read.
Environment scoping
Because environment is part of the namespace, the same tenant can run genuinely independent values per environment — Cedar off in production, on in staging — without a bolt-on 'is this staging' check scattered through application code.
Ordered targeting rules
A real flag has explicit deny rules, explicit allow overrides, segment matches, a percentage rollout, and a default — evaluated in a documented order, so exactly one rule wins and the evaluator can report which one and why.
Deterministic Percentage Rollouts
A naive rollout calls Math.random() < 0.10 on every request. The same user flips between ON and OFF on consecutive requests — a bug, not a rollout.
Replace randomness with a deterministic hash of a stable tuple — tenant, project, environment, flag key, bucketing unit, the subject's stable ID, and a flag-specific salt — reduced to a uniform number, say zero to 99,999. In this illustrative example, User 42 at Birch hashes to bucket 7,314; the ten percent rollout covers buckets 0–9,999, so User 42 is ON, consistently, on every instance, because the inputs never change.
Monotonic expansion
Raising the rollout from ten percent to twenty percent moves the threshold from bucket 10,000 to bucket 20,000. User 42 (bucket 7,314) was already inside the old range and stays ON; the change only adds newly eligible buckets — it never evicts a user who was already in, as long as the salt and algorithm stay fixed.
Governed Publication & Distribution
An edit is a draft until a reviewer approves it. Once approved, the control plane commits the durable change and writes an outbox event in the same transaction, so a snapshot builder can turn it into an immutable, versioned artifact — never a silent mutation of a previous version.
Poll with a safety net, push for freshness
SDKs poll on an interval using a conditional request (cheap 304 when nothing changed); an added push/notification channel improves typical freshness, but is treated as a hint to go fetch the authoritative snapshot, never as the update itself — polling stays as the fallback that catches a missed push.
Monotonic SDK activation
An SDK never activates a version older than the one it's already running, even if a delayed message delivers it after a newer version is already active. This single rule is what makes an unreliable, asynchronous distribution path safe to build on top of.
Outages, Kill Switches & Rollback
A control-plane outage must not interrupt an already-warmed SDK's evaluation — that's the entire payoff of moving evaluation local. A brand-new, cold instance with no snapshot yet is the one case that needs an explicit, documented startup-default policy per flag.
Rollback never rewrites history: an operator reverting a problematic rollout publishes a brand-new version whose content matches the last known-good configuration, and then watches version adoption across the fleet to confirm the rollback has actually reached production traffic.
Security: Flags Are Not Authorization
Backend services can safely hold the full targeting rule set in memory. A browser is a different trust boundary — it receives only its own already-evaluated result, never the underlying segment definitions or other tenants' rules.
Attribute minimization
Send only the user attributes targeting genuinely needs for evaluation; never raw PII when a derived attribute would do.
Flags guide behavior, not access
A flag can safely decide whether a user sees a new UI. It must never be the sole gate in front of a protected operation — that still requires an independently verified permission check, enforced server-side regardless of flag state.
Final Architecture
Every component below exists because of a specific, demonstrated failure earlier in this story. A small, single-tenant deployment may not need every piece at once.
- 01Control Plane: authentication, RBAC, validation, approval workflow, and publish orchestration
- 02Config DB: tenant/project/environment-scoped transactional store for flag definitions and rules
- 03Outbox: transactional record of publish events, written atomically with the config commit
- 04Snapshot Builder: produces immutable, versioned, validated configuration artifacts
- 05Distribution: CDN/relay fan-out serving regional poll and push traffic
- 06SDK: local snapshot cache with monotonic version activation
- 07Evaluator: pure, in-memory, deterministic rule and bucketing engine
- 08Telemetry: async, nonblocking exposure/evaluation event pipeline