Skip to content
SeeItFlow
System Design · 11 chapters · ~29 min read

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.

Mangesh MandlikLast updated: How lessons are made
Chapter 01

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.

Swipe to pan
One Deployment, Three Incompatible DemandsAtlas wants ON, Birch wants a 10% pilot, Cedar wants OFF — all today, all on the same binaryTenant Atlaswants: ON for allTenant Birchwants: 10% rolloutTenant Cedarwants: OFF (prod)Northstar Appone deployed binaryNo `if` statement can satisfy all three at once
One deployed binary, three tenants, three incompatible requirements — all today

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.

Interactive

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.

Chapter 02

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.

Chapter 03

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.

Swipe to pan
Deployment ≠ ReleaseTwo separate decisions, on two separate schedulesDeployship code to prod — constant, low-riskReleaseturn behavior on for users — deliberate, governedA feature flag is the mechanism that decouples these two schedules from each other
Deployment and release are different decisions — V0 conflates them into one

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.

Chapter 04

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.

Swipe to pan
A Config Outage Becomes an App OutageEvery request pays a synchronous round trip just to check a flagAppFlags DBunreachable500 errorA component meant to makereleases safer just becamethe outage itself
A synchronous, per-request dependency on a remote flags database turns a config outage into an application outage

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.

Chapter 05

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.

Chapter 06

Tenant Isolation: Namespace & Authorization

Introduce a real namespace: tenant → project → environment. Every flag definition, override, and published snapshot is scoped to this exact tuple.

Swipe to pan
Rule Precedence: Exactly One Winning ReasonDocumented, tested order — not implicit code-path orderingDeny ruleExplicit allowSegment match% rolloutDefault
A documented, tested precedence — deny beats allow beats segment beats percentage beats default

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.

Chapter 07

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.

Swipe to pan
Deterministic Bucketing (illustrative)hash(tenant, flag, userId, salt) mod 100,000 → a stable bucketuser42 · flag · salthash()bucket 7,314/ 100,0000–9,99910% rollout threshold — bucket 7,314 falls inside it → ON
hash(tenant, flag, userId, salt) mod 100,000 → a stable bucket (illustrative example)

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.

Chapter 08

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.

Chapter 09

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.

Chapter 10

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.

Chapter 11

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.

Swipe to pan
Feature Flag Platform, Final ArchitectureData plane (solid, synchronous) + control plane (dashed, asynchronous)App / SDKEvaluatorin-memoryDistributionCDN / relaySnapshot BuilderOutboxConfig DBControl Planeauth · approve · publishTelemetryasync
Data plane (solid, synchronous) and control plane (dashed, asynchronous) — the complete feature flag platform
Key takeaways
  1. 01Control Plane: authentication, RBAC, validation, approval workflow, and publish orchestration
  2. 02Config DB: tenant/project/environment-scoped transactional store for flag definitions and rules
  3. 03Outbox: transactional record of publish events, written atomically with the config commit
  4. 04Snapshot Builder: produces immutable, versioned, validated configuration artifacts
  5. 05Distribution: CDN/relay fan-out serving regional poll and push traffic
  6. 06SDK: local snapshot cache with monotonic version activation
  7. 07Evaluator: pure, in-memory, deterministic rule and bucketing engine
  8. 08Telemetry: async, nonblocking exposure/evaluation event pipeline