ct;
export interface FlagDefinition {
key: string;
defaultValue: FlagValue;
variants: Record<string, FlagValue>;
rules?: EvaluationRule[];
bucketingKey?: string;
salt?: string;
metadata?: Record<string, unknown>;
}
export interface EvaluationRule {
condition: Condition;
value: FlagValue;
variant?: string;
}
export interface Condition {
attribute: string;
operator: 'eq' | 'neq' | 'gt' | 'lt' | 'in' | 'exists';
value: any;
}
#### 2. Build the Evaluation Engine
The engine resolves the flag value based on rules and context. This should be deterministic for the same context to ensure consistent user experiences.
```typescript
import { createHash } from 'crypto';
export interface EvaluationContext {
userId?: string;
environment: 'development' | 'staging' | 'production';
attributes: Record<string, any>;
}
export interface EvaluationResult<T extends FlagValue = FlagValue> {
value: T;
variant?: string;
reason: 'DEFAULT' | 'RULE' | 'TARGETING' | 'ERROR';
metadata: Record<string, unknown>;
}
export class FeatureFlagEngine {
private flags: Map<string, FlagDefinition> = new Map();
private cache: Map<string, EvaluationResult> = new Map();
private cacheTTL: number = 60000; // 60 seconds
constructor(flags: FlagDefinition[]) {
flags.forEach(f => this.flags.set(f.key, f));
}
evaluate<T extends FlagValue>(
key: string,
context: EvaluationContext,
defaultValue?: T
): EvaluationResult<T> {
const cacheKey = `${key}:${JSON.stringify(context)}`;
const cached = this.cache.get(cacheKey);
if (cached && (Date.now() - cached.metadata.timestamp < this.cacheTTL)) {
return cached as EvaluationResult<T>;
}
const flag = this.flags.get(key);
if (!flag) {
return this.buildResult(defaultValue as T, 'DEFAULT', { error: 'Flag not found' });
}
try {
// Rule evaluation
if (flag.rules) {
for (const rule of flag.rules) {
if (this.evaluateCondition(rule.condition, context)) {
const result = this.buildResult(
rule.value as T,
'RULE',
{ ruleIndex: flag.rules.indexOf(rule) }
);
this.cache.set(cacheKey, result);
return result;
}
}
}
// Bucketing for percentage rollouts
if (context.userId && flag.bucketingKey) {
const hash = this.hash(context.userId, flag.salt || '');
const bucket = parseInt(hash, 16) % 100;
// Assuming rules handle percentage via attribute or custom logic
// Simplified bucketing logic for demonstration
}
const result = this.buildResult(flag.defaultValue as T, 'DEFAULT', {});
this.cache.set(cacheKey, result);
return result;
} catch (error) {
return this.buildResult(
(defaultValue ?? flag.defaultValue) as T,
'ERROR',
{ error: error.message }
);
}
}
private evaluateCondition(condition: Condition, context: EvaluationContext): boolean {
const value = this.getAttributeValue(condition.attribute, context);
switch (condition.operator) {
case 'eq': return value === condition.value;
case 'neq': return value !== condition.value;
case 'gt': return value > condition.value;
case 'lt': return value < condition.value;
case 'in': return Array.isArray(condition.value) && condition.value.includes(value);
case 'exists': return value !== undefined && value !== null;
default: return false;
}
}
private getAttributeValue(attribute: string, context: EvaluationContext): any {
if (attribute === 'userId') return context.userId;
if (attribute === 'environment') return context.environment;
return context.attributes[attribute];
}
private hash(input: string, salt: string): string {
return createHash('sha256').update(input + salt).digest('hex');
}
private buildResult<T extends FlagValue>(
value: T,
reason: EvaluationResult['reason'],
metadata: Record<string, unknown>
): EvaluationResult<T> {
return {
value,
reason,
metadata: { ...metadata, timestamp: Date.now() }
};
}
}
3. Integration and SDK Pattern
Wrap the engine in a client that handles initialization and updates.
export class FeatureFlagClient {
private engine: FeatureFlagEngine;
private configProvider: ConfigProvider;
constructor(provider: ConfigProvider) {
this.configProvider = provider;
this.engine = new FeatureFlagEngine([]);
}
async initialize(): Promise<void> {
const config = await this.configProvider.fetch();
this.engine = new FeatureFlagEngine(config.flags);
}
isEnabled(key: string, context: EvaluationContext, defaultVal = false): boolean {
const result = this.engine.evaluate<boolean>(key, context, defaultVal);
this.emitMetric(key, context, result);
return result.value;
}
private emitMetric(key: string, context: EvaluationContext, result: EvaluationResult) {
// Integration with analytics/observability system
// e.g., analytics.track('flag_evaluated', { key, variant: result.variant, reason: result.reason });
}
}
interface ConfigProvider {
fetch(): Promise<{ flags: FlagDefinition[] }>;
}
4. Rationale for Architecture
- Deterministic Evaluation: The use of hashing for bucketing ensures that the same user always sees the same variant, critical for A/B testing validity.
- Caching: Reduces evaluation latency to O(1) for repeated calls within the TTL window.
- Error Handling: The engine falls back to defaults on errors, ensuring system resilience.
- Observability: Metrics emission allows tracking flag usage and performance impact.
Pitfall Guide
1. Flag Rot
Mistake: Failing to remove flags after a feature is fully rolled out.
Impact: Dead code increases bundle size, complicates logic, and creates maintenance debt.
Best Practice: Implement automated CI checks that scan for flags older than a defined threshold (e.g., 30 days) and fail the build. Use naming conventions that include expiration dates.
2. Flag Explosion
Mistake: Creating too many flags without managing combinations.
Impact: Combinatorial explosion makes testing impossible. A system with 15 flags has 32,768 states.
Best Practice: Limit active flags per service. Use flag groups or namespaces. Prioritize cleanup. Document flag dependencies.
3. Hardcoded Defaults Without Fallbacks
Mistake: Assuming the flag service is always available.
Impact: Service outage if the flag provider fails.
Best Practice: Always define sensible defaults. Implement circuit breakers and local caching strategies. Use "kill switches" that default to a safe state (e.g., feature off).
4. Mixing Release and Operational Flags
Mistake: Using the same mechanism for long-term configuration and short-term releases.
Impact: Operational flags get lost in release noise; release flags become permanent config.
Best Practice: Categorize flags:
- Release Flags: Short-lived, remove after rollout.
- Operational Flags: Long-lived, manage system behavior (e.g., rate limiting thresholds).
- Experiment Flags: Time-bound, require statistical analysis.
5. Client-Side Security Leaks
Mistake: Exposing internal features or sensitive logic in client-side flag payloads.
Impact: Users can reverse-engineer the SDK to enable unreleased features or access restricted data.
Best Practice: Never evaluate sensitive logic client-side. Use server-side evaluation for security-critical flags. Filter payloads sent to client SDKs.
6. Ignoring Evaluation Latency
Mistake: Making synchronous remote calls for flag evaluation on every request.
Impact: Increased P99 latency and dependency on external service availability.
Best Practice: Use streaming connections or background polling to update local cache. Evaluate against local cache synchronously.
7. Lack of Auditability
Mistake: No record of who changed a flag and when.
Impact: Inability to debug incidents caused by flag changes.
Best Practice: Integrate flag changes with audit logs. Require approval workflows for production flag toggles.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple A/B Test | Client-Side SDK with Remote Config | Fast UI rendering; low server overhead | Low (SDK cost) |
| Kill Switch / Safety | Server-Side Evaluation with Local Cache | Immediate effect; high reliability; secure | Medium (Infra) |
| Enterprise SaaS | Hybrid Architecture + Segmentation | Granular control per tenant; security compliance | High (Platform/Tooling) |
| Legacy Monolith | Ad-hoc Flags with Strict Lifecycle | Quick win; low migration effort | Low (Dev time) |
| High-Scale Microservices | Distributed Flag Service + Streaming | Low latency; consistency across services | High (Engineering) |
Configuration Template
Use this TypeScript configuration to bootstrap a flag service with best practices.
import { FeatureFlagClient, FlagDefinition, EvaluationContext } from './flag-engine';
// 1. Define Flags
const FLAGS: FlagDefinition[] = [
{
key: 'checkout.new-ui',
defaultValue: false,
variants: { on: true, off: false },
rules: [
{
condition: { attribute: 'environment', operator: 'eq', value: 'staging' },
value: true,
variant: 'on'
},
{
condition: { attribute: 'userId', operator: 'in', value: ['user-123', 'user-456'] },
value: true,
variant: 'beta'
}
],
metadata: {
owner: 'checkout-team',
expiresAt: '2024-06-01',
type: 'release'
}
},
{
key: 'payments.rate-limit-multiplier',
defaultValue: 1.0,
variants: { standard: 1.0, aggressive: 0.5 },
metadata: {
owner: 'payments-team',
type: 'operational'
}
}
];
// 2. Initialize Client
const client = new FeatureFlagClient({
fetch: async () => ({ flags: FLAGS })
});
await client.initialize();
// 3. Usage Example
const context: EvaluationContext = {
userId: 'user-789',
environment: 'production',
attributes: {
tier: 'premium',
region: 'us-east-1'
}
};
if (client.isEnabled('checkout.new-ui', context)) {
// Render new UI
} else {
// Render legacy UI
}
Quick Start Guide
- Install/Setup: Integrate the
FeatureFlagClient into your service. If using a third-party provider, install their SDK and initialize with your environment key.
- Define First Flag: Create a flag definition in your configuration or provider dashboard. Set a safe default value.
- Wrap Code: Replace the conditional logic in your code with the client evaluation method. Pass the necessary context.
- Toggle: Use the dashboard or API to enable the flag for a specific user or percentage. Verify behavior in the target environment.
- Monitor & Remove: Observe metrics and logs. Once the feature is stable and fully rolled out, remove the flag code and definition. Update the CI pipeline to prevent re-introduction.