# Feature Flags as Architecture: How to Manage Progressive Rollouts with OpenFeature and Flagsmith

Most teams meet feature flags as a simple `if` statement, wrap new code in a condition, flip it on when ready. That works for the first five flags. By the fiftieth, you have stale conditionals scattered across the codebase, no record of who owns what, and a vendor SDK imported in two hundred files.

Treating flags as **architecture** changes the picture. A flag becomes a deliberate control point that separates *deploying* code from *releasing* it, lets you expose changes to 1% of users before 100%, and gives you a kill switch when something goes wrong at 3am.

This guide shows how to build that foundation with two tools that fit together well, **OpenFeature**, a vendor-neutral API for feature flagging, and **Flagsmith**, an open-source flag management platform that supplies the targeting rules and rollout controls.

## Flags Are Not All the Same
Before writing code, classify your flags. Different types have different lifespans and owners:

| Flag type | Purpose | Typical lifespan |
|-----------|---------|------------------|
| **Release** | Hide unfinished or risky features, roll out gradually | Days to weeks |
| **Operational** | Kill switches, load shedding, degraded modes | Long-lived |
| **Experiment** | A/B tests tied to a metric | Weeks |
| **Permission** | Entitlements by plan or tenant | Long-lived |

The most common source of flag debt is a *release* flag that quietly becomes permanent. Decide the type at creation, and you can decide the expiry at the same moment.

## Why OpenFeature?
If your code calls a vendor SDK directly, switching vendors, or even upgrading, means touching every call site. **OpenFeature** is an open specification that defines a standard flag-evaluation API. Your application codes against OpenFeature, a **provider** translates those calls to whichever backend you use.

```
Application code
      │  getBooleanValue("new-checkout", false, context)
      ▼
OpenFeature SDK  (stable, vendor-neutral API)
      │
      ▼
Provider  (Flagsmith, or any other backend)
      │
      ▼
Flag management platform  (rules, segments, rollouts)
```

Flagsmith has OpenFeature providers for both server-side Node.js and client-side JavaScript, so you get its targeting engine without coupling your business logic to its SDK.

## Step 1 - Server-Side Setup
```bash
npm install @openfeature/server-sdk @openfeature/flagsmith-provider flagsmith-nodejs
```

```typescript
// src/flags/bootstrap.ts
import { OpenFeature } from '@openfeature/server-sdk';
import FlagsmithOpenFeatureProvider from '@openfeature/flagsmith-provider';
import Flagsmith from 'flagsmith-nodejs'; // check your SDK version's import style

export async function initFlags() {
  const flagsmith = new Flagsmith({
    environmentKey: process.env.FLAGSMITH_ENVIRONMENT_KEY!,
  });

  const provider = new FlagsmithOpenFeatureProvider(flagsmith);

  // Wait for the provider to be ready before serving traffic
  await OpenFeature.setProviderAndWait(provider);
}
```

Call `initFlags()` during application startup, before your server begins accepting requests, so the first evaluations don't race provider initialization.

## Step 2 - Evaluate Flags with Context
Flag decisions depend on *who* is asking. OpenFeature models this as **evaluation context**, with a `targetingKey` identifying the subject:

```typescript
// src/checkout/checkout.service.ts
import { OpenFeature } from '@openfeature/server-sdk';

const client = OpenFeature.getClient('checkout');

export async function selectCheckoutFlow(user: User) {
  const useNewCheckout = await client.getBooleanValue(
    'new-checkout',
    false, // safe default if evaluation fails
    {
      targetingKey: user.id,
      plan: user.plan,
      country: user.country,
    },
  );

  return useNewCheckout ? newCheckout(user) : legacyCheckout(user);
}
```

Two design points matter here:

**Always provide a safe default.** The second argument is returned if the provider is unavailable or the flag is missing. Choose the value that keeps your system in its known-good state, usually the *old* behavior.

**Use a stable targeting key.** Percentage rollouts hash the key, so the same user consistently lands in the same bucket. A random or per-request key makes users flip between experiences.

## Step 3 - Progressive Rollouts
With the code in place, the rollout itself happens in Flagsmith, not in your repository. You configure **segments** (groups of users defined by traits) and a **percentage split** on the flag, then widen exposure in stages without redeploying.

A practical rollout plan:

| Stage | Audience | Hold until |
|-------|----------|-----------|
| 1 | Internal staff segment | No functional defects found |
| 2 | 1–5% of users | Error rate and latency match baseline |
| 3 | 25% of users | Business metrics stable |
| 4 | 100% of users | Flag marked for removal |

The gates between stages should be **measurable**, not a feeling. Compare error rate, latency, and a key business metric for the flagged cohort against the control group. If you already track SLOs, a rollout that burns error budget faster than expected is your signal to pause or roll back, by flipping a flag, not by shipping a revert.

> **Flags vs. deployment-level canaries.** Tools like Argo Rollouts shift *traffic between versions*. Flags shift *behavior within one version*. They complement each other, use canary deployments to validate the build, and flags to control exposure of specific features inside it.

## Step 4 - A Typed Flag Registry
Raw string keys scattered through the codebase invite typos and make cleanup painful. Centralize them:

```typescript
// src/flags/registry.ts
import { OpenFeature } from '@openfeature/server-sdk';

export const FLAGS = {
  newCheckout:   { key: 'new-checkout',   default: false, owner: 'payments', type: 'release' },
  searchRanking: { key: 'search-ranking-v2', default: false, owner: 'search', type: 'experiment' },
  disableRecs:   { key: 'disable-recommendations', default: false, owner: 'platform', type: 'operational' },
} as const;

const client = OpenFeature.getClient();

export function isEnabled(
  flag: keyof typeof FLAGS,
  context: { targetingKey: string; [k: string]: string },
) {
  const f = FLAGS[flag];
  return client.getBooleanValue(f.key, f.default, context);
}
```

The registry doubles as documentation, every flag has an owner and a type in one searchable file, which makes the lifecycle practices below enforceable.

## Step 5 - Observability with Hooks
A flag that changes behavior should be visible in your telemetry, or you'll spend incidents asking "was that flag on?" OpenFeature **hooks** let you attach cross-cutting behavior to every evaluation:

```typescript
// src/flags/telemetry.hook.ts
import type { Hook, HookContext, EvaluationDetails, FlagValue } from '@openfeature/server-sdk';

export const telemetryHook: Hook = {
  after(ctx: HookContext, details: EvaluationDetails<FlagValue>) {
    logger.info({
      event: 'flag_evaluated',
      flag: ctx.flagKey,
      value: details.value,
      reason: details.reason,
    });
  },
  error(ctx: HookContext, err: unknown) {
    logger.warn({ event: 'flag_error', flag: ctx.flagKey, err });
  },
};

OpenFeature.addHooks(telemetryHook);
```

Add the active flag values as attributes on your traces, and you can slice latency and errors by flag state, the quickest way to confirm whether a rollout caused a regression.

## Step 6 - Client-Side Flags
For browser or mobile apps, use the OpenFeature web SDK with Flagsmith's client provider:

```typescript
import { OpenFeature } from '@openfeature/web-sdk';
import { FlagsmithClientProvider } from '@openfeature/flagsmith-client-provider';

OpenFeature.setProvider(
  new FlagsmithClientProvider({ environmentID: process.env.FLAGSMITH_ENV_ID! }),
);

await OpenFeature.setContext({ targetingKey: user.id, traits: { plan: user.plan } });
```

**One rule is non-negotiable:** **client-side flags are visible to users.** Anyone can inspect the browser and read or tamper with flag values. Use them for UI behavior, never for security decisions such as access control, enforce those on the server.

## Step 7 - Managing Flag Lifecycle
Flag debt is the main long-term cost of this architecture, and it's a process problem more than a technical one.

**Assign an owner and an expiry at creation.** Every release flag should have a removal date. The registry above already stores the owner and type.

**Make cleanup part of "done."** A rollout isn't finished at 100%, it's finished when the flag and the dead code path are deleted.

**Automate reminders.** A scheduled job can compare flags in your registry against their age and open issues for owners:

```yaml
# .github/workflows/flag-audit.yml
on:
  schedule:
    - cron: '0 9 * * 1'   # weekly
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: node scripts/audit-flags.js   # flags older than 30 days → GitHub issues
```

**Test both branches.** Every live flag doubles your code paths. Run tests with each state for flags that gate critical behavior.

## Common Pitfalls
**Nested and interdependent flags.** When flag B only makes sense if flag A is on, combinations multiply and become untestable. Keep flags independent, or collapse them into one.

**No safe default.** If the flag service is unreachable and your default is the *new* behavior, an outage in a third-party service enables unreleased code in production.

**Using flags as long-term configuration.** Permanent settings belong in configuration or entitlements, not in a system built for temporary release control.

**Evaluating flags in hot loops.** Evaluate once per request and pass the result down, rather than calling the provider repeatedly.

**Skipping the audit trail.** Know who changed a flag and when. Flag changes are production changes and deserve the same traceability as deployments.

## Conclusion
Feature flags earn their place in your architecture when they're treated as controlled, owned, and temporary mechanisms rather than ad hoc conditionals. OpenFeature keeps your code independent of any one vendor, and Flagsmith provides the segments and percentage rollouts that make gradual release practical.

Start small, pick one risky change, put it behind a flag with a safe default, roll it out in stages with measurable gates, and delete the flag when you're done. That single loop, deploy, expose gradually, verify, clean up, is the habit that makes releases boring, which is exactly what you want.

*Using LaunchDarkly, Unleash, or a homegrown flag system today? OpenFeature's provider model lets you migrate gradually, even running multiple providers side by side. Share your current setup in the comments.*

#FeatureFlags #OpenFeature #Flagsmith #ProgressiveDelivery #ContinuousDelivery

