Back to Blog

Feature Flags Implementation: LaunchDarkly, Unleash, and Building Your Own

Implement feature flags for safe deployments, A/B testing, and gradual rollouts. Compare LaunchDarkly vs Unleash vs custom solutions with TypeScript code exampl

Viprasol Tech Team
April 1, 2026
12 min read

Feature Flags Implementation: LaunchDarkly, Unleash, and Building Your Own

Feature flags decouple deployment from release. You merge code to main, ship it to production, and then turn the feature on for 1% of users โ€” then 10%, then 100% โ€” watching metrics the whole time. If something breaks, you flip the flag off without a rollback deploy.

This matters more as teams scale. When 20 engineers are shipping code daily, every deploy is a risk surface. Feature flags let you ship continuously while releasing deliberately.


What Feature Flags Enable

Use CaseHow Flags Help
Gradual rolloutEnable for 1% โ†’ 10% โ†’ 50% โ†’ 100% of users
Kill switchInstantly disable a broken feature without a redeploy
A/B testingRoute different users to different implementations
Beta programsEnable for specific user IDs or organization IDs
Ops flagsToggle maintenance mode, rate limiting, circuit breakers
Regional rolloutEnable in US before EU, or vice versa
Internal testingEnable only for employees before external release

The Anatomy of a Feature Flag

A flag evaluation takes context (who is asking) and returns a value (what they should see):

// Simplest case โ€” boolean flag
const isEnabled = await flags.getBooleanValue('new-checkout-flow', userId, false);

// Multivariate โ€” returns one of several values
const variant = await flags.getStringValue(
  'pricing-page-layout',
  userId,
  'control'  // default
);
// Returns: 'control' | 'variant-a' | 'variant-b'

// Number flag โ€” useful for rate limits, thresholds
const rateLimit = await flags.getNumberValue('api-rate-limit-rpm', userId, 100);

The flag key, the user context (for targeting), and a default value (for when the flag service is unavailable) are the three required inputs to every evaluation.


๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Option 1: LaunchDarkly

LaunchDarkly is the most feature-rich managed option. It has sophisticated targeting rules, a streaming SDK that pushes flag changes to running servers in milliseconds, and built-in experimentation.

Node.js SDK setup:

// lib/flags.ts
import * as ld from '@launchdarkly/node-server-sdk';

const ldClient = ld.init(process.env.LAUNCHDARKLY_SDK_KEY!, {
  // Use LaunchDarkly's flag streaming (default) for instant updates
  // Set offline: true for tests
});

await ldClient.waitForInitialization({ timeout: 10 });

export interface FlagUser {
  key: string;          // Unique user ID โ€” used for consistent bucketing
  email?: string;
  custom?: Record<string, string | boolean | number>;
}

export async function getBooleanFlag(
  flagKey: string,
  user: FlagUser,
  defaultValue: boolean
): Promise<boolean> {
  const context: ld.LDContext = {
    kind: 'user',
    key: user.key,
    email: user.email,
    ...user.custom,
  };
  return ldClient.variation(flagKey, context, defaultValue);
}

export async function getStringFlag(
  flagKey: string,
  user: FlagUser,
  defaultValue: string
): Promise<string> {
  const context: ld.LDContext = { kind: 'user', key: user.key, email: user.email };
  return ldClient.variation(flagKey, context, defaultValue);
}

// For React โ€” use the LaunchDarkly React SDK
// import { useLDClient, useFlags } from 'launchdarkly-react-client-sdk';

Middleware to attach flags to request context:

// middleware/featureFlags.ts
import { FastifyRequest, FastifyReply } from 'fastify';
import { getBooleanFlag, getStringFlag } from '@/lib/flags';

declare module 'fastify' {
  interface FastifyRequest {
    flags: {
      newCheckoutFlow: boolean;
      pricingVariant: string;
      apiRateLimit: number;
    };
  }
}

export async function featureFlagMiddleware(
  request: FastifyRequest,
  reply: FastifyReply
) {
  const userId = request.headers['x-user-id'] as string ?? 'anonymous';
  const userEmail = request.headers['x-user-email'] as string;
  const user = { key: userId, email: userEmail };

  // Evaluate all flags needed for this request in parallel
  const [newCheckoutFlow, pricingVariant, apiRateLimit] = await Promise.all([
    getBooleanFlag('new-checkout-flow', user, false),
    getStringFlag('pricing-page-variant', user, 'control'),
    getBooleanFlag('increased-rate-limit', user, false),
  ]);

  request.flags = {
    newCheckoutFlow,
    pricingVariant,
    apiRateLimit: apiRateLimit ? 500 : 100,
  };
}

Option 2: Unleash (Self-Hosted)

Unleash is open-source and self-hosted โ€” you own the flag server, the data never leaves your infrastructure, and there's no per-seat or per-request pricing. The tradeoff is operational overhead and a less polished UI.

Deploying Unleash on Docker:

# docker-compose.yml for Unleash
version: '3.8'
services:
  unleash-db:
    image: postgres:16
    environment:
      POSTGRES_DB: unleash
      POSTGRES_USER: unleash
      POSTGRES_PASSWORD: ${UNLEASH_DB_PASSWORD}
    volumes:
      - unleash-data:/var/lib/postgresql/data

  unleash:
    image: unleashorg/unleash-server:latest
    environment:
      DATABASE_URL: postgresql://unleash:${UNLEASH_DB_PASSWORD}@unleash-db/unleash
      INIT_FRONTEND_API_TOKENS: default:development.unleash-insecure-frontend-api-token
      INIT_CLIENT_API_TOKENS: default:development.unleash-insecure-api-token
    ports:
      - "4242:4242"
    depends_on:
      - unleash-db

volumes:
  unleash-data:

Node.js client:

// lib/unleash.ts
import { initialize } from 'unleash-client';

const unleash = initialize({
  url: process.env.UNLEASH_URL!,           // e.g., https://unleash.internal/api
  appName: 'api-service',
  customHeaders: {
    Authorization: process.env.UNLEASH_API_TOKEN!,
  },
  // Sync interval โ€” how often to poll for flag changes
  refreshInterval: 15,  // seconds
  metricsInterval: 60,
});

await new Promise<void>((resolve, reject) => {
  unleash.on('synchronized', resolve);
  unleash.on('error', reject);
  setTimeout(reject, 10_000);  // Fail fast on startup
});

export function isEnabled(
  flagName: string,
  userId: string,
  sessionId?: string
): boolean {
  return unleash.isEnabled(flagName, {
    userId,
    sessionId,
    remoteAddress: undefined,
  });
}

export function getVariant(flagName: string, userId: string): string {
  const variant = unleash.getVariant(flagName, { userId });
  return variant.enabled ? variant.name : 'disabled';
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Option 3: Build Your Own (Minimal)

For small teams or simple use cases, a custom implementation with PostgreSQL + Redis is often the right choice. No vendor dependency, no per-seat cost, full control.

// lib/customFlags.ts
import Redis from 'ioredis';
import { db } from './db';

const redis = new Redis(process.env.REDIS_URL!);
const FLAG_CACHE_TTL = 30; // seconds

interface FlagRule {
  type: 'percentage' | 'user_ids' | 'email_domain' | 'always';
  percentage?: number;   // 0โ€“100 for percentage rollout
  userIds?: string[];    // Allowlist of specific user IDs
  emailDomain?: string;  // e.g., 'viprasol.com' for internal only
}

interface FeatureFlag {
  key: string;
  enabled: boolean;
  rules: FlagRule[];
  defaultValue: boolean;
}

async function getFlagConfig(key: string): Promise<FeatureFlag | null> {
  // Check cache first
  const cached = await redis.get(`flag:${key}`);
  if (cached) return JSON.parse(cached);

  // Load from DB
  const flag = await db.featureFlag.findUnique({ where: { key } });
  if (!flag) return null;

  const config: FeatureFlag = {
    key: flag.key,
    enabled: flag.enabled,
    rules: flag.rules as FlagRule[],
    defaultValue: flag.defaultValue,
  };

  await redis.setex(`flag:${key}`, FLAG_CACHE_TTL, JSON.stringify(config));
  return config;
}

export async function evaluateFlag(
  key: string,
  userId: string,
  userEmail?: string
): Promise<boolean> {
  const flag = await getFlagConfig(key);
  if (!flag || !flag.enabled) return flag?.defaultValue ?? false;

  for (const rule of flag.rules) {
    switch (rule.type) {
      case 'always':
        return true;

      case 'user_ids':
        if (rule.userIds?.includes(userId)) return true;
        break;

      case 'email_domain':
        if (userEmail?.endsWith(`@${rule.emailDomain}`)) return true;
        break;

      case 'percentage':
        // Consistent bucketing โ€” same user always gets same result
        const hash = hashUserId(`${key}:${userId}`);
        const bucket = hash % 100;
        if (bucket < (rule.percentage ?? 0)) return true;
        break;
    }
  }

  return flag.defaultValue;
}

// Simple consistent hash (djb2)
function hashUserId(input: string): number {
  let hash = 5381;
  for (let i = 0; i < input.length; i++) {
    hash = (hash * 33) ^ input.charCodeAt(i);
  }
  return Math.abs(hash);
}

Admin API to invalidate cache when a flag changes:

// api/admin/flags/[key]/route.ts
export async function PUT(request: Request, { params }: { params: { key: string } }) {
  const body = await request.json();
  
  await db.featureFlag.upsert({
    where: { key: params.key },
    create: { key: params.key, ...body },
    update: body,
  });

  // Invalidate cache immediately
  await redis.del(`flag:${params.key}`);

  return Response.json({ success: true });
}

Comparison

FactorLaunchDarklyUnleashCustom
Setup time1 hour1โ€“2 days1โ€“2 weeks
Targeting sophisticationExcellentGoodYou build it
A/B testing / experimentsBuilt-inPluginYou build it
Real-time flag updates< 200ms (streaming)~15s (poll)Depends on impl
Analytics / audit logExcellentGoodYou build it
Self-hostedNoYesYes
Data residencyLaunchDarkly's cloudYour infraYour infra
Cost$12โ€“35/seat/moFree (self-host)Engineering time
Operational overheadNoneMediumLow

Flag Hygiene: Avoiding Flag Debt

Flags accumulate. A codebase with 200 dead flags is a maintenance nightmare โ€” every if (flagEnabled(...)) branch adds cognitive overhead and testing complexity.

Rules that prevent flag debt:

  1. Every flag has an owner and a removal date โ€” set when created
  2. Flags live in code for at most 90 days after reaching 100% rollout
  3. Flag names encode intent: new-checkout-flow not feature-123
  4. Permanent flags get a different treatment: ops flags (maintenance mode, rate limits) live indefinitely but are documented separately from release flags
  5. CI checks for stale flags: Lint rules that error if a flag key hasn't been evaluated in N days
// eslint-plugin-custom/rules/no-stale-flags.ts
// Checks flag keys against a registry with removal dates
// Fails CI if a flag's removal date has passed

Cost Summary

OptionMonthly Cost (10 engineers)Notes
LaunchDarkly Starter$120/mo3 seats, limited experiments
LaunchDarkly Pro$350โ€“600/moFull targeting, experiments
Unleash Enterprise$150โ€“300/moManaged hosting
Unleash self-hosted$40โ€“80/mo (infra)Engineering time to operate
Custom$0 (infra ~$20/mo)2โ€“3 weeks to build initially

Working With Viprasol

We implement feature flag systems as part of our deployment infrastructure work โ€” either integrating LaunchDarkly/Unleash or building a custom solution when client requirements (data residency, cost at scale) make that the better fit.

โ†’ Talk to our DevOps team about deployment safety and feature flags.


See Also

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.