logic.
2. OpenFeature Abstraction: Direct integration with specific flag providers (e.g., LaunchDarkly, Unleash) creates vendor lock-in. OpenFeature provides a unified API for flag evaluation, allowing the underlying provider to be swapped without code changes.
3. Evaluation Context: Flags must be evaluated against a rich context object containing user, device, and request metadata. Context construction must be decoupled from evaluation to allow reuse and testing.
4. Local Caching & Streaming: To avoid latency, the SDK must maintain a local cache of flag definitions and use streaming connections to update the cache in real-time. Network calls should only occur for dynamic user segmentation rules, not flag retrieval.
Step-by-Step Implementation
1. Define Evaluation Context Interface
The context must be typed and extensible. This ensures consistency across services.
// src/flags/context.ts
import { EvaluationContext } from '@openfeature/server-sdk';
export interface AppEvaluationContext extends EvaluationContext {
tenantId: string;
userId?: string;
plan: 'free' | 'pro' | 'enterprise';
region: string;
isBetaTester: boolean;
}
export function buildContext(request: Request): AppEvaluationContext {
return {
targetingKey: request.user?.id || request.headers['x-request-id'],
tenantId: request.headers['x-tenant-id'],
userId: request.user?.id,
plan: request.user?.plan || 'free',
region: process.env.DEPLOYMENT_REGION || 'us-east-1',
isBetaTester: request.user?.isBetaTester || false,
};
}
2. Implement Flag Evaluation Service
Create a service that wraps OpenFeature calls. This service handles default values and error fallbacks, ensuring the application degrades gracefully if the flag provider is unavailable.
// src/flags/feature-flag.service.ts
import { OpenFeature, Client } from '@openfeature/server-sdk';
export class FeatureFlagService {
private client: Client;
constructor(provider: any) {
// Initialize OpenFeature with a provider (e.g., Unleash, LaunchDarkly)
OpenFeature.setProvider(provider);
this.client = OpenFeature.getClient('backend-core');
}
async getBooleanFlag(
flagKey: string,
context: AppEvaluationContext,
defaultValue: boolean
): Promise<boolean> {
try {
const details = await this.client.getBooleanDetails(flagKey, defaultValue, context);
// Log variation for analytics/audit
if (details.variation !== undefined) {
this.auditLog(flagKey, details);
}
return details.value;
} catch (error) {
// Fallback to default value on provider failure
console.error(`Flag evaluation failed for ${flagKey}, using default.`, error);
return defaultValue;
}
}
private auditLog(flagKey: string, details: any): void {
// Emit metric or log for flag usage analysis
// Example: metrics.increment('flag.evaluated', { flag: flagKey, variation: details.variation });
}
}
3. Integrate with Request Pipeline
Use middleware or decorators to inject flag evaluation into API routes or workers. This keeps business logic clean.
// src/middleware/flag-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { FeatureFlagService } from '../flags/feature-flag.service';
import { buildContext } from '../flags/context';
export function requireFlag(flagKey: string, flagService: FeatureFlagService) {
return async (req: Request, res: Response, next: NextFunction) => {
const context = buildContext(req);
const isEnabled = await flagService.getBooleanFlag(flagKey, context, false);
if (!isEnabled) {
return res.status(403).json({ error: 'Feature not available' });
}
// Attach flag state to request for downstream use
req.flags = { ...req.flags, [flagKey]: true };
next();
};
}
4. Usage in Business Logic
Flags can guard specific code paths without cluttering the core logic.
// src/services/payment.service.ts
import { FeatureFlagService } from '../flags/feature-flag.service';
export class PaymentService {
constructor(private flagService: FeatureFlagService) {}
async processPayment(txnId: string, context: AppEvaluationContext) {
const useNewProcessor = await this.flagService.getBooleanFlag(
'payment.new-processor-v2',
context,
false
);
if (useNewProcessor) {
return this.processWithNewProcessor(txnId);
}
return this.processWithLegacyProcessor(txnId);
}
}
Rationale
- Graceful Degradation: The
try/catch block ensures that flag provider outages do not block application functionality. Defaults are strictly defined.
- Context Reuse: Context building is separated, allowing the same context to be used for multiple flag evaluations within a single request, reducing overhead.
- Auditability: Logging variations enables tracking of flag usage, which is essential for identifying zombie flags and analyzing rollout impact.
Pitfall Guide
1. Flag Sprawl and Zombie Flags
Mistake: Creating flags without expiration dates or retirement plans. Over time, the codebase becomes littered with dead code paths guarded by flags that are permanently on or off.
Best Practice: Enforce expiration dates in the flag management UI. Integrate flag audits into the CI/CD pipeline. Use tools that detect flags that have not changed state in 90 days.
2. Evaluation Latency Spikes
Mistake: Configuring the SDK to fetch flag values over HTTP for every request. This introduces network latency and creates a dependency on the flag provider's availability for every API call.
Best Practice: Always use local evaluation mode. Ensure the SDK streams flag definitions to a local cache. Evaluation should occur in-memory with sub-millisecond latency.
3. Context Leakage and PII
Mistake: Passing sensitive user data (PII) in the evaluation context, which may be logged by the flag provider or exposed in error messages.
Best Practice: Hash or tokenize sensitive identifiers before adding them to the context. Review the flag provider's data residency and compliance policies. Sanitize context objects before logging.
4. Flag Combinatorics Explosion
Mistake: Relying on multiple interacting flags within the same code path. With N flags, there are 2^N possible states, making testing impossible and increasing the risk of undefined behavior.
Best Practice: Limit the number of active flags per feature. Use flag dependencies (parent/child flags) to reduce combinations. Avoid nested flag checks; flatten logic where possible.
5. Inadequate Testing Strategies
Mistake: Testing only the "happy path" where flags are enabled. Failing to test disabled states, default values, and provider failures.
Best Practice: Parameterize tests to run against all flag variations. Mock the flag provider to simulate outages and verify fallback behavior. Include flag state in integration test matrices.
6. Missing Fallback Defaults
Mistake: Assuming the flag provider will always return a value. If the SDK fails to initialize or the provider returns an error, the application may crash or exhibit undefined behavior.
Best Practice: Every flag evaluation must specify a explicit default value. The default should represent the safe state for the system (e.g., false for destructive features, true for performance optimizations).
7. Lack of Rollback Runbooks
Mistake: Treating flags as "set and forget." When a flag causes an issue, engineers scramble to find the flag key or lack permissions to toggle it.
Best Practice: Maintain a runbook linking features to their flag keys. Ensure on-call engineers have read/write access to the flag dashboard. Automate "panic button" scripts that can disable critical flags instantly.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / Small Team | SaaS Provider (LaunchDarkly) | Low operational overhead, rich UI, fast setup. | Higher per-MRU cost; scales with usage. |
| High Volume / Cost Sensitive | Self-Hosted (Unleash) + OpenFeature | Full control, no per-MRU fees, data sovereignty. | Engineering cost for hosting/maintenance. |
| Multi-Cloud / Vendor Agnostic | OpenFeature + Multi-Provider | Prevents lock-in, allows provider switching. | Initial abstraction overhead; negligible runtime cost. |
| Strict Compliance / Air-Gapped | Self-Hosted + Local Eval | Data never leaves infrastructure; eval is offline. | Infrastructure cost; requires robust sync strategy. |
Configuration Template
OpenFeature Provider Configuration (TypeScript)
This template demonstrates initializing OpenFeature with a generic provider, configuring caching, and setting up the client.
// src/flags/openfeature-config.ts
import { OpenFeature } from '@openfeature/server-sdk';
import { MyFlagProvider } from './providers/my-flag-provider'; // Your provider implementation
export async function initializeFeatureFlags() {
const provider = new MyFlagProvider({
// Provider specific options
streamingEnabled: true,
pollingInterval: 60000, // Fallback polling if streaming fails
logger: console,
});
await OpenFeature.setProviderAndWait(provider);
// Configure global metadata
OpenFeature.setMetadata({
name: 'backend-api',
version: '1.0.0',
});
console.log('Feature Flags initialized successfully.');
}
// Usage in application bootstrap
import { initializeFeatureFlags } from './flags/openfeature-config';
async function bootstrap() {
await initializeFeatureFlags();
// Start server...
}
bootstrap();
Quick Start Guide
-
Install Dependencies:
npm install @openfeature/server-sdk
npm install <provider-sdk> # e.g., @openfeature/unleash-provider
-
Initialize Provider:
Create a provider instance and set it on OpenFeature during application startup. Ensure streaming is enabled for real-time updates.
-
Create Evaluation Service:
Implement a service class that wraps OpenFeature.getClient(). Add methods for getBoolean, getString, and getNumber with typed defaults.
-
Integrate Middleware:
Add flag evaluation middleware to your router. Pass the request context to the service and guard routes based on flag state.
-
Verify and Monitor:
Deploy the changes. Toggle a flag in the provider dashboard. Verify the application responds to the change within seconds. Check logs for evaluation latency and errors.
Backend feature flags are a force multiplier for engineering velocity and safety, but only when implemented with rigorous architecture and disciplined lifecycle management. By adopting provider-agnostic standards, enforcing local evaluation, and automating flag hygiene, teams can unlock progressive delivery without accumulating technical debt.