Production-grade feature flags using OpenFeature
The first time you need a feature flag, the answer feels obvious: add a column to your database, or drop a value in your .env file. It…
Production-grade feature flags using OpenFeature

The first time you need a feature flag, the answer feels obvious: add a column to your database, or drop a value in your .env file. It works. You ship. Six months later, you have 40 flags scattered across three systems, a Redis cache nobody fully understands, and a comment in the codebase that says // DO NOT TOUCH.
While this works for a weekend project, it falls apart in a high-traffic production environment for two main reasons:
- The database tax. If you have a feature flag check in a high-frequency middleware, every request hits your database. At 1,000 requests per second, you’re adding 1,000 extra queries just to read a boolean. That’s a real bottleneck.
- The caching trap. The obvious fix is adding Redis. But now you’re managing two systems, and you’ve introduced a classic problem: when you update a flag in Postgres, how quickly does Redis reflect that? Do you manually bust the cache? How do you avoid stale reads? This adds real architectural complexity just to manage a few toggles, and notably, it’s still your team’s problem to solve.
Dedicated flagging engines like [flagd](https://flagd.dev/) solve this differently. Instead of querying a database per request, flagd runs as a sidecar process next to your app. It keeps flag configurations in memory and receives updates via a push stream, so there's no cache to bust and no database load. The key distinction from a roll-your-own Redis cache: the invalidation logic is handled for you, not bolted on by you.
What exactly is OpenFeature?
Think of OpenFeature as a standard driver for feature flagging.
When you write a Node.js app that needs a database, you don’t write “Postgres-specific” code in every single route. Instead, you use a standard library (like an ORM or a driver) that lets you write your queries once. If you decide to switch from Postgres to MySQL tomorrow, you don’t rewrite your entire app. You just swap the driver at the entry point.
OpenFeature is that same abstraction layer, but for feature flags.
The main benefit of OpenFeature is that it decouples your application code from the feature flag backend implementation. Without it, your business logic becomes “polluted” with code specific to a particular flagging service or tool.
How it breaks down:
To make this work, OpenFeature uses three simple concepts:
- The SDK (The Interface): This is the code you write in your routes. It’s a standard way to ask, “Is this feature on?” or “What configuration should I use?”
- The Provider (The Driver): This is the “bridge.” It’s a small plugin that tells the SDK how to fetch flags from your specific source (whether that’s a local JSON file or a sophisticated cloud engine).
- The Evaluation Context: This is the metadata (such as a user’s location or ID) that you pass at runtime so the provider can make a smart decision.
By using this approach, you aren’t “married” to any specific setup. You can use a simple JSON file for local development and then swap in a robust production engine by changing exactly one line of code where you initialize the provider.
Let’s see it in action: Smart AI Routing
To show you why this matters, let’s look at a “2026 problem”: AI Model Orchestration.
Imagine you want to route users to different LLMs based on their location. Maybe users in India should hit GPT-5.5, while users in the US get Claude 4.7 Sonnet. Instead of hardcoding this logic, we’ll let OpenFeature handle it.
1. The Setup: flagd
We’ll use flagd, an open-source feature flag evaluation engine. It’s essentially a sidecar that holds our “targeting rules” in a simple JSON file.
Create a simple file called flag.json with the following content
{
"flags": {
"llm_config": {
"state": "ENABLED",
"variants": {
"india_config": { "provider": "openai", "model": "gpt-5.5" },
"us_config": { "provider": "anthropic", "model": "claude-4.7-sonnet" }
},
"defaultVariant": "india_config",
"targeting": {
"if": [
{ "==": [{ "var": "location" }, "US"] },
"us_config",
"india_config"
]
}
}
}
}
2. The Code: Express + OpenFeature
Here’s how clean your Node.js code looks when you use the standard. We’ll initialize the provider once and then just “ask” for the flag.
import "dotenv/config";
import express, { type Request, type Response } from "express";
import { OpenFeature } from "@openfeature/server-sdk";
import { FlagdProvider } from "@openfeature/flagd-provider";
import { type ChatRequest, type ChatRequestBody } from "./types/index.js";
import { providers } from "./utils/provider.js";
import { openFeatureMiddleware } from "./middlewares/openfeature.js";
const PORT = process.env.PORT ?? 3000;
const FLAGD_HOST = process.env.FLAGD_HOST ?? "localhost";
const FLAGD_PORT = Number(process.env.FLAGD_PORT) || 8013;
// THIS IS THE ONLY FUNCTION THAT NEEDS TO BE UPDATE
// IF YOU CHANGE YOUR FEATURE FLAG BACKEND
async function initFeatureFlags(): Promise<void> {
const provider = new FlagdProvider({
host: FLAGD_HOST,
port: FLAGD_PORT,
});
await OpenFeature.setProviderAndWait(provider);
console.log(`✅ OpenFeature connected to flagd at ${FLAGD_HOST}:${FLAGD_PORT}`);
}
const app = express();
app.use(express.json());
app.use(openFeatureMiddleware);
app.post("/chat", async (req: Request, res: Response) => {
const chatReq = req as ChatRequest;
const { prompt } = req.body as ChatRequestBody;
if (!prompt || typeof prompt !== "string") {
res.status(400).json({
error: "A `prompt` string is required in the request body.",
});
return;
}
const { provider: providerName, model } = chatReq.llmConfig;
const providerAdapter = providers[providerName];
if (!providerAdapter) {
res.status(500).json({
error: `Unknown provider "${providerName}" resolved from feature flag.`,
});
return;
}
try {
const response = await providerAdapter.chat(prompt, model);
res.json({
response: response.toJSON(),
routing: {
resolvedProvider: providerName,
resolvedModel: model,
evaluationContext: chatReq.evaluationContext,
},
});
} catch (err) {
const error = err as Error;
console.error(`Provider "${providerName}" call failed:`, error);
res.status(502).json({
error: `LLM provider "${providerName}" returned an error.`,
details: error.message,
});
}
});
async function main(): Promise<void> {
try {
await initFeatureFlags();
} catch (err) {
const error = err as Error;
console.warn("⚠️ Could not connect to flagd — using default flag values.", error.message);
}
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log(` POST /chat — send { "prompt": "..." } with x-location header`);
console.log(` GET /health — health check`);
});
}
main();
3. Leveraging “Evaluation Context”
This is the “killer feature” of OpenFeature. We pass the user’s location into the evaluation context, and the SDK handles the rest.
For this particular example, I have created a simple middleware that uses OpenFeature to determine which model configuration should be used before the request even reaches our main logic.
import { type Request, type Response, type NextFunction } from "express";
import { OpenFeature } from "@openfeature/server-sdk";
import { ChatRequest, LLMConfig } from "../types/index.js";
export const openFeatureMiddleware = async (req: Request, _res: Response, next: NextFunction) => {
const chatReq = req as ChatRequest;
try {
const locationHeader = req.headers["x-location"];
const location = (Array.isArray(locationHeader) ? locationHeader[0] : locationHeader) ?? "IN";
const client = OpenFeature.getClient();
const defaultConfig: LLMConfig = { provider: "openai", model: "gpt-5.5" };
const llmConfig = await client.getObjectValue("llm_config", defaultConfig, {
location,
});
// Attach to the request so downstream handlers can use it
chatReq.llmConfig = llmConfig as LLMConfig;
chatReq.evaluationContext = { location };
next();
} catch (err) {
console.error("Feature flag evaluation failed:", err);
// Graceful degradation — fall back to default config
const locationHeader = req.headers["x-location"];
const location = (Array.isArray(locationHeader) ? locationHeader[0] : locationHeader) ?? "IN";
chatReq.llmConfig = { provider: "openai", model: "gpt-5.5" };
chatReq.evaluationContext = { location };
next();
}
};
By using the Evaluation Context, we’ve effectively separated our infrastructure concerns from our business logic. Here is what’s happening under the hood:
- Dynamic Decision Making: The middleware doesn’t contain a single
if/elsestatement regarding region-based routing. It simply passes thelocationto OpenFeature and says, "Here is the context; you tell me what the config should be." - The “Provider-Blind” Controller: Because the resolved
llmConfigis attached directly to the request object, our/chatroute doesn't need to know why it was assigned a specific model. It just consumes the result. - Built-in Resilience: Notice the
try/catchblock. Ifflagdgoes offline or the network blips, the application doesn't crash. It falls back to a safedefaultConfig, ensuring a seamless user experience even during infrastructure hiccups.
4. The Payoff: Swapping the Engine
This is where the “driver” analogy becomes reality. Imagine your team decides to move away from a local JSON file to a Postgres backend or a massive enterprise flagging service.
In a traditional setup, you’d be performing a “search and replace” across your entire codebase to swap SDKs. With OpenFeature, you only change the initialization logic.
Your middleware, your /chat route, and your business logic stay 100% the same. The only thing that changes is your initFeatureFlags function:
// Swapping the 'engine' is a one-line change
async function initFeatureFlags(): Promise<void> {
// Instead of FlagdProvider, we drop in a different provider
// const provider = new PostgresProvider({ ... });
const provider = new CloudFlagProvider({ apiToken: '...' });
await OpenFeature.setProviderAndWait(provider);
}
Using a standard like OpenFeature isn’t just about clean code; it’s about operational maturity. At scale, this setup gives you:
- Instant Kill Switches: If a new model version (like GPT-5.5) starts hallucinating or becomes too expensive, you flip the flag in your backend, and the entire fleet shifts to Claude 4.7 Sonnet in milliseconds, no redeploy required.
- Safe Canary Releases: You can roll out a new AI provider to 5% of users in a specific region, monitor the latency, and scale up only when you’re confident.
- Zero Vendor Lock-in: You aren’t “married” to any specific tool. You own your code, and the providers are just pluggable utilities.
Is it worth the setup?
For a solo project or internal tool, probably not. Environment variables are fine. But if you’re on a team shipping to real users, the question isn’t whether you need structured feature flags, it’s when you’ll wish you had them.
OpenFeature gives you the structure without locking you into any particular backend. Start with a local JSON file. Graduate to a hosted service when the time comes. Your application code doesn’t care either way.
Find the full source code on GitHub: github.com/Nikhiladiga/openfeature-flagd
메타데이터
- post_id
- f75bb98a673e
- slug
- production-grade-feature-flags-using-openfeature-f75bb98a673e
- url
- https://medium.com/@nikhiladigaz/production-grade-feature-flags-using-openfeature-f75bb98a673e
- canonical_url
- https://medium.com/@nikhiladigaz/production-grade-feature-flags-using-openfeature-f75bb98a673e
- author_url
- https://medium.com/@nikhiladigaz
- status
- ok
- fetched_at
- 2026-06-09 14:34:10