Back to Journal
SaaS Engineering

Complete Guide to Feature Flag Architecture with Typescript

A comprehensive guide to implementing Feature Flag Architecture using Typescript, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 20 min read

TypeScript's type system provides a unique advantage for feature flag architecture: flag configurations, targeting rules, and evaluation results can be fully typed end-to-end, catching integration errors at compile time. This guide covers building a comprehensive feature flag system in TypeScript that leverages the type system for safety while maintaining sub-millisecond evaluation performance.

Type-Safe Flag Definitions

Define flags as typed constants that serve as both configuration and documentation:

typescript
1// flags/definitions.ts
2export const FLAG_DEFINITIONS = {
3 "new-checkout-flow": {
4 type: "release" as const,
5 description: "Redesigned checkout with single-page flow",
6 owner: "payments-team",
7 defaultVariant: "control",
8 variants: ["control", "single-page", "multi-step"] as const,
9 createdAt: "2025-01-15",
10 expiresAt: "2025-03-15",
11 },
12 "premium-analytics": {
13 type: "permission" as const,
14 description: "Advanced analytics dashboard for paid plans",
15 owner: "analytics-team",
16 defaultVariant: "disabled",
17 variants: ["disabled", "basic", "advanced"] as const,
18 },
19 "circuit-breaker-payments": {
20 type: "ops" as const,
21 description: "Kill switch for payment processing",
22 owner: "sre-team",
23 defaultVariant: "enabled",
24 variants: ["enabled", "disabled"] as const,
25 },
26} as const;
27 
28export type FlagKey = keyof typeof FLAG_DEFINITIONS;
29export type FlagVariant<K extends FlagKey> = (typeof FLAG_DEFINITIONS)[K]["variants"][number];
30 
31// Type-safe evaluation result
32export interface TypedEvalResult<K extends FlagKey> {
33 enabled: boolean;
34 variant: FlagVariant<K>;
35 reason: string;
36}
37 

This approach means calling evaluate("new-checkout-flow", ctx) returns a result where variant is typed as "control" | "single-page" | "multi-step" — not just string.

Evaluation Engine

typescript
1import { createHash } from "crypto";
2 
3interface Condition {
4 attribute: string;
5 operator: "eq" | "neq" | "in" | "contains" | "starts_with" | "gt" | "lt";
6 values: string[];
7}
8 
9interface Rule {
10 conditions: Condition[];
11 variant: string;
12 priority: number;
13}
14 
15interface FlagConfig {
16 key: string;
17 enabled: boolean;
18 percentage: number;
19 rules: Rule[];
20 variants: Record<string, number>;
21 defaultVariant: string;
22}
23 
24interface EvalContext {
25 userId: string;
26 email?: string;
27 plan?: string;
28 country?: string;
29 properties?: Record<string, string>;
30}
31 
32interface EvalResult {
33 enabled: boolean;
34 variant: string;
35 reason: string;
36}
37 
38class FlagEvaluator {
39 private flags: Map<string, FlagConfig> = new Map();
40 
41 update(configs: FlagConfig[]): void {
42 const newFlags = new Map<string, FlagConfig>();
43 for (const config of configs) {
44 newFlags.set(config.key, config);
45 }
46 this.flags = newFlags; // Atomic swap
47 }
48 
49 evaluate(flagKey: string, context: EvalContext): EvalResult {
50 const flag = this.flags.get(flagKey);
51 if (!flag) {
52 return { enabled: false, variant: "", reason: "not_found" };
53 }
54 if (!flag.enabled) {
55 return { enabled: false, variant: "", reason: "disabled" };
56 }
57 
58 // Rule-based targeting (sorted by priority descending)
59 const sortedRules = [...flag.rules].sort((a, b) => b.priority - a.priority);
60 for (const rule of sortedRules) {
61 if (this.matchesAll(rule.conditions, context)) {
62 return { enabled: true, variant: rule.variant, reason: "rule_match" };
63 }
64 }
65 
66 // Percentage rollout
67 if (flag.percentage > 0 && flag.percentage < 100) {
68 const bucket = this.hashBucket(flagKey, context.userId);
69 if (bucket < flag.percentage) {
70 return { enabled: true, variant: flag.defaultVariant, reason: "percentage" };
71 }
72 return { enabled: false, variant: "", reason: "percentage_excluded" };
73 }
74 
75 return { enabled: true, variant: flag.defaultVariant, reason: "default" };
76 }
77 
78 isEnabled(flagKey: string, context: EvalContext): boolean {
79 return this.evaluate(flagKey, context).enabled;
80 }
81 
82 private hashBucket(flagKey: string, userId: string): number {
83 const hash = createHash("sha256")
84 .update(`${flagKey}:${userId}`)
85 .digest();
86 const value = hash.readUInt32BE(0);
87 return (value / 0xffffffff) * 100;
88 }
89 
90 private matchesAll(conditions: Condition[], ctx: EvalContext): boolean {
91 return conditions.every((c) => this.matchCondition(c, ctx));
92 }
93 
94 private matchCondition(cond: Condition, ctx: EvalContext): boolean {
95 const value = this.getAttribute(cond.attribute, ctx);
96 switch (cond.operator) {
97 case "eq": return cond.values[0] === value;
98 case "neq": return cond.values[0] !== value;
99 case "in": return cond.values.includes(value);
100 case "contains": return value.includes(cond.values[0]);
101 case "starts_with": return value.startsWith(cond.values[0]);
102 default: return false;
103 }
104 }
105 
106 private getAttribute(attr: string, ctx: EvalContext): string {
107 const map: Record<string, string | undefined> = {
108 user_id: ctx.userId,
109 email: ctx.email,
110 plan: ctx.plan,
111 country: ctx.country,
112 };
113 return map[attr] ?? ctx.properties?.[attr] ?? "";
114 }
115}
116 

React Integration

A React provider with hooks for flag evaluation:

typescript
1import { createContext, useContext, useMemo, ReactNode } from "react";
2 
3interface FlagContextValue {
4 isEnabled: (flagKey: string) => boolean;
5 getVariant: (flagKey: string) => string;
6 evaluatedFlags: Record<string, EvalResult>;
7}
8 
9const FlagContext = createContext<FlagContextValue | null>(null);
10 
11export function FlagProvider({
12 children,
13 evaluator,
14 context,
15}: {
16 children: ReactNode;
17 evaluator: FlagEvaluator;
18 context: EvalContext;
19}) {
20 const value = useMemo(() => {
21 const cache: Record<string, EvalResult> = {};
22 
23 return {
24 isEnabled: (flagKey: string) => {
25 if (!cache[flagKey]) {
26 cache[flagKey] = evaluator.evaluate(flagKey, context);
27 }
28 return cache[flagKey].enabled;
29 },
30 getVariant: (flagKey: string) => {
31 if (!cache[flagKey]) {
32 cache[flagKey] = evaluator.evaluate(flagKey, context);
33 }
34 return cache[flagKey].variant;
35 },
36 evaluatedFlags: cache,
37 };
38 }, [evaluator, context]);
39 
40 return <FlagContext.Provider value={value}>{children}</FlagContext.Provider>;
41}
42 
43export function useFlag(flagKey: string): boolean {
44 const ctx = useContext(FlagContext);
45 if (!ctx) throw new Error("useFlag must be used within FlagProvider");
46 return ctx.isEnabled(flagKey);
47}
48 
49export function useFlagVariant(flagKey: string): string {
50 const ctx = useContext(FlagContext);
51 if (!ctx) throw new Error("useFlagVariant must be used within FlagProvider");
52 return ctx.getVariant(flagKey);
53}
54 
55// Usage in components
56function CheckoutPage() {
57 const useNewCheckout = useFlag("new-checkout-flow");
58 const variant = useFlagVariant("new-checkout-flow");
59 
60 if (!useNewCheckout) return <LegacyCheckout />;
61 
62 switch (variant) {
63 case "single-page": return <SinglePageCheckout />;
64 case "multi-step": return <MultiStepCheckout />;
65 default: return <LegacyCheckout />;
66 }
67}
68 

Need a second opinion on your saas engineering architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Next.js Server Component Integration

typescript
1// app/lib/flags.ts
2import { cookies, headers } from "next/headers";
3 
4const evaluator = new FlagEvaluator();
5 
6export async function getServerFlags(): Promise<{
7 isEnabled: (key: string) => boolean;
8 getVariant: (key: string) => string;
9}> {
10 const headerStore = await headers();
11 const userId = headerStore.get("x-user-id") ?? "";
12 const plan = headerStore.get("x-plan") ?? "";
13 
14 const context: EvalContext = { userId, plan };
15 const cache: Record<string, EvalResult> = {};
16 
17 return {
18 isEnabled: (key: string) => {
19 if (!cache[key]) cache[key] = evaluator.evaluate(key, context);
20 return cache[key].enabled;
21 },
22 getVariant: (key: string) => {
23 if (!cache[key]) cache[key] = evaluator.evaluate(key, context);
24 return cache[key].variant;
25 },
26 };
27}
28 
29// app/dashboard/page.tsx
30export default async function DashboardPage() {
31 const flags = await getServerFlags();
32 
33 return (
34 <div>
35 <h1>Dashboard</h1>
36 {flags.isEnabled("analytics-dashboard") && <AnalyticsWidget />}
37 {flags.isEnabled("ai-insights") && <AIInsightsPanel />}
38 </div>
39 );
40}
41 

Sync Client

typescript
1class FlagSyncClient {
2 private evaluator: FlagEvaluator;
3 private apiUrl: string;
4 private intervalMs: number;
5 private etag: string | null = null;
6 private timer: NodeJS.Timeout | null = null;
7 
8 constructor(evaluator: FlagEvaluator, apiUrl: string, intervalMs = 10_000) {
9 this.evaluator = evaluator;
10 this.apiUrl = apiUrl;
11 this.intervalMs = intervalMs;
12 }
13 
14 async start(): Promise<void> {
15 await this.sync();
16 this.timer = setInterval(() => this.sync(), this.intervalMs);
17 }
18 
19 stop(): void {
20 if (this.timer) clearInterval(this.timer);
21 }
22 
23 private async sync(): Promise<void> {
24 const headers: Record<string, string> = {};
25 if (this.etag) headers["If-None-Match"] = this.etag;
26 
27 const response = await fetch(`${this.apiUrl}/api/flags`, { headers });
28 if (response.status === 304) return;
29 
30 const configs: FlagConfig[] = await response.json();
31 this.evaluator.update(configs);
32 this.etag = response.headers.get("etag");
33 }
34}
35 

Testing

typescript
1describe("FlagEvaluator", () => {
2 let evaluator: FlagEvaluator;
3 
4 beforeEach(() => {
5 evaluator = new FlagEvaluator();
6 });
7 
8 test("percentage rollout has correct distribution", () => {
9 evaluator.update([{ key: "test", enabled: true, percentage: 50, rules: [], variants: {}, defaultVariant: "" }]);
10 
11 let enabled = 0;
12 for (let i = 0; i < 10_000; i++) {
13 if (evaluator.isEnabled("test", { userId: `user-${i}` })) enabled++;
14 }
15 expect(enabled / 100).toBeGreaterThanOrEqual(48);
16 expect(enabled / 100).toBeLessThanOrEqual(52);
17 });
18 
19 test("rule targeting overrides percentage", () => {
20 evaluator.update([{
21 key: "enterprise-feature",
22 enabled: true,
23 percentage: 0,
24 rules: [{ conditions: [{ attribute: "plan", operator: "in", values: ["enterprise"] }], variant: "full", priority: 1 }],
25 variants: {},
26 defaultVariant: "",
27 }]);
28 
29 expect(evaluator.isEnabled("enterprise-feature", { userId: "1", plan: "enterprise" })).toBe(true);
30 expect(evaluator.isEnabled("enterprise-feature", { userId: "2", plan: "free" })).toBe(false);
31 });
32});
33 

Conclusion

TypeScript's type system transforms feature flag architecture from a stringly-typed configuration problem into a compile-time-checked system. Typed flag definitions ensure that variant handling is exhaustive, evaluation contexts have correct attributes, and flag keys are validated at build time. The React hook pattern integrates naturally with component-based UIs, while Next.js server components enable flag evaluation without client-side JavaScript overhead.

The evaluation engine itself is straightforward — a Map lookup followed by rule matching and hash-based bucketing. TypeScript adds zero overhead to the evaluation logic since types erase at compile time. Combined with a periodic sync client that uses ETag-based caching, the system handles thousands of evaluations per second with negligible latency impact.

FAQ

Need expert help?

Building with saas engineering?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026