Uncategorized

Feature Flagging: using LaunchDarkly or open‑source alternatives

Feature Flagging Using LaunchDarkly Or Open‑source Alternatives

Introduction

Shipping new features can be risky, especially when they affect critical user flows like checkout, authentication, or payment processing. Feature flagging helps you release safely by turning features on or off without redeploying your application. Companies like Netflix, GitHub, and Etsy use feature flags to deploy code hundreds of times per day while maintaining stability. As a result, teams gain more control over rollouts, testing, and experimentation. In this comprehensive guide, we will explore how feature flagging works, when to use it, and how tools like LaunchDarkly, Unleash, Flagsmith, and GrowthBook can support a smooth, controlled release process with practical code examples for multiple languages and frameworks.

What Is Feature Flagging

A feature flag (also called feature toggle or feature switch) is a conditional switch in your code that decides whether a feature should be active. Instead of shipping everything at once and hoping for the best, you wrap new features in flags and toggle them when ready.

// Simple feature flag concept
if (featureFlags.isEnabled('new-checkout')) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}

This enables gradual rollouts, A/B testing, and safe experimentation without code changes or redeployments. Furthermore, if something goes wrong, you can disable the feature instantly without rolling back your deployment.

Types of Feature Flags

Release flags – Control feature visibility during rollout (short-lived)
Experiment flags – A/B testing and multivariate experiments (medium-lived)
Ops flags – Circuit breakers and kill switches (long-lived)
Permission flags – User-tier features like premium vs free (long-lived)

Common Use Cases

Gradual rollouts – Deploy to 1%, then 10%, then 50%, then 100%
Beta testing – Enable features for specific user segments
A/B testing – Compare different implementations with metrics
Feature tiers – Premium vs free feature access
Trunk-based development – Deploy incomplete work safely behind flags
Kill switches – Instantly disable problematic features
Canary releases – Test with internal users before public release

LaunchDarkly: Enterprise Feature Management

LaunchDarkly is the industry-leading platform for feature flag management. It provides real-time toggles, user targeting, experimentation, and analytics through a clean dashboard.

Key Features

Real-time flag updates without redeployment (sub-200ms propagation)
User-based targeting and complex segmentation rules
Multivariate flags for A/B/n testing
Built-in experimentation with statistical analysis
Audit logs for compliance and debugging
SDKs for 25+ languages and frameworks
Edge computing support for low-latency evaluation

LaunchDarkly Node.js Example

// Install: npm install @launchdarkly/node-server-sdk

import * as LaunchDarkly from '@launchdarkly/node-server-sdk';

// Initialize the client
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);

// Wait for initialization
await ldClient.waitForInitialization();

console.log('LaunchDarkly client initialized');

// Define user context
const context = {
  kind: 'user',
  key: 'user-123',
  email: 'alice@example.com',
  name: 'Alice',
  custom: {
    plan: 'premium',
    country: 'US',
    signupDate: '2024-01-15'
  }
};

// Evaluate a boolean flag
const showNewDashboard = await ldClient.variation(
  'new-dashboard',
  context,
  false // default value if flag not found
);

if (showNewDashboard) {
  renderNewDashboard();
} else {
  renderLegacyDashboard();
}

// Evaluate a multivariate flag (string)
const checkoutVersion = await ldClient.variation(
  'checkout-experiment',
  context,
  'control'
);

switch (checkoutVersion) {
  case 'control':
    renderOriginalCheckout();
    break;
  case 'variant-a':
    renderSimplifiedCheckout();
    break;
  case 'variant-b':
    renderOnePageCheckout();
    break;
  default:
    renderOriginalCheckout();
}

// Evaluate a JSON flag for configuration
const featureConfig = await ldClient.variation(
  'pricing-tiers',
  context,
  { basic: 9.99, pro: 29.99, enterprise: 99.99 }
);

console.log(`Pro tier price: $${featureConfig.pro}`);

// Track custom events for experiments
ldClient.track('checkout-completed', context, {
  revenue: 49.99,
  items: 3
});

// Clean shutdown
await ldClient.close();

LaunchDarkly React Example

// Install: npm install launchdarkly-react-client-sdk

import { withLDProvider, useFlags, useLDClient } from 'launchdarkly-react-client-sdk';

// Wrap your app with the provider
const App = () => {
  return (
    
      
    
  );
};

export default withLDProvider({
  clientSideID: process.env.REACT_APP_LD_CLIENT_ID,
  context: {
    kind: 'user',
    key: 'anonymous',
  },
  options: {
    bootstrap: 'localStorage', // Use cached flags initially
  }
})(App);

// Use flags in components
function Dashboard() {
  const { newDashboard, betaFeatures, experimentVariant } = useFlags();
  const ldClient = useLDClient();

  // Update user context when they log in
  useEffect(() => {
    if (user) {
      ldClient.identify({
        kind: 'user',
        key: user.id,
        email: user.email,
        name: user.name,
        custom: {
          plan: user.subscription,
          company: user.company
        }
      });
    }
  }, [user, ldClient]);

  // Track events
  const handlePurchase = (amount) => {
    ldClient.track('purchase-completed', { revenue: amount });
  };

  if (newDashboard) {
    return ;
  }

  return ;
}

// Conditional rendering with flags
function FeatureCard({ feature }) {
  const { betaFeatures } = useFlags();

  return (
    
      

{feature.name}

{feature.description}

{betaFeatures && feature.isBeta && ( Beta )}
); }

Unleash: Open-Source Feature Management

Unleash is one of the most popular open-source feature flag platforms, offering self-hosted or cloud deployment options.

Unleash Features

Self-hosted or cloud deployment options
Role-based access control for teams
Advanced targeting rules with strategies
SDKs for all major languages
Metrics and analytics
API-first design

Unleash Docker Setup

# docker-compose.yml
version: '3.8'

services:
  unleash:
    image: unleashorg/unleash-server:latest
    ports:
      - "4242:4242"
    environment:
      DATABASE_URL: postgres://unleash:password@db:5432/unleash
      DATABASE_SSL: "false"
      LOG_LEVEL: info
      INIT_ADMIN_API_TOKENS: "*:*.unleash-admin-token"
    depends_on:
      - db
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:4242/health || exit 1
      interval: 10s
      timeout: 5s
      retries: 5

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: unleash
      POSTGRES_USER: unleash
      POSTGRES_PASSWORD: password
    volumes:
      - unleash_data:/var/lib/postgresql/data

volumes:
  unleash_data:

Unleash Node.js Example

// Install: npm install unleash-client

import { initialize, isEnabled, getVariant } from 'unleash-client';

// Initialize the client
const unleash = initialize({
  url: 'http://localhost:4242/api/',
  appName: 'my-app',
  instanceId: 'my-instance-1',
  customHeaders: {
    Authorization: process.env.UNLEASH_API_TOKEN,
  },
});

// Wait for ready
unleash.on('ready', () => {
  console.log('Unleash is ready');
});

unleash.on('error', (err) => {
  console.error('Unleash error:', err);
});

// Check if feature is enabled
const context = {
  userId: 'user-123',
  sessionId: 'session-abc',
  properties: {
    plan: 'premium',
    country: 'US'
  }
};

if (isEnabled('new-checkout', context)) {
  console.log('New checkout enabled');
} else {
  console.log('Using legacy checkout');
}

// Get variant for A/B testing
const variant = getVariant('checkout-experiment', context);

console.log(`Variant: ${variant.name}`);
console.log(`Payload: ${JSON.stringify(variant.payload)}`);

switch (variant.name) {
  case 'A':
    renderVariantA();
    break;
  case 'B':
    renderVariantB();
    break;
  default:
    renderControl();
}

Flagsmith: API-First Feature Flags

Flagsmith offers both cloud-hosted and self-hosted options with an API-first approach and excellent frontend SDK support.

Flagsmith Features

User segments and remote config
API-first approach for flexibility
Strong frontend and mobile SDK support
Audit logs and approval workflows
Environment-based configuration

Flagsmith JavaScript Example

// Install: npm install flagsmith

import flagsmith from 'flagsmith';

// Initialize with environment ID
await flagsmith.init({
  environmentID: process.env.FLAGSMITH_ENV_ID,
  onChange: (oldFlags, params) => {
    console.log('Flags updated:', params);
  },
});

// Identify user for targeting
await flagsmith.identify('user-123', {
  email: 'alice@example.com',
  plan: 'premium',
  company_size: 50
});

// Check boolean flag
if (flagsmith.hasFeature('new-dashboard')) {
  renderNewDashboard();
}

// Get feature value (remote config)
const maxUploadSize = flagsmith.getValue('max-upload-size', 10); // default 10MB
console.log(`Max upload: ${maxUploadSize}MB`);

// Get all flags
const allFlags = flagsmith.getAllFlags();
console.log('All flags:', allFlags);

// React hook integration
import { FlagsmithProvider, useFlags, useFlagsmith } from 'flagsmith/react';

function App() {
  return (
    
      
    
  );
}

function Dashboard() {
  const flags = useFlags(['new-dashboard', 'beta-features']);
  const flagsmith = useFlagsmith();

  useEffect(() => {
    if (user) {
      flagsmith.identify(user.id, { plan: user.plan });
    }
  }, [user]);

  return (
    
{flags.new_dashboard?.enabled && }
); }

GrowthBook: Experimentation-Focused

GrowthBook is an open-source A/B testing and feature flagging tool that excels at experiment analysis with statistical rigor.

GrowthBook Features

Easy integration with minimal setup
Detailed experiment analytics with Bayesian statistics
SDK for React, Node, Python, and more
Visual editor for non-developers
Data warehouse integration

GrowthBook React Example

// Install: npm install @growthbook/growthbook-react

import { GrowthBook, GrowthBookProvider, useFeatureIsOn, useFeatureValue } from '@growthbook/growthbook-react';

// Create GrowthBook instance
const gb = new GrowthBook({
  apiHost: 'https://cdn.growthbook.io',
  clientKey: process.env.REACT_APP_GROWTHBOOK_KEY,
  enableDevMode: process.env.NODE_ENV === 'development',
  trackingCallback: (experiment, result) => {
    // Send to analytics
    analytics.track('Experiment Viewed', {
      experimentId: experiment.key,
      variationId: result.key,
    });
  },
});

// Load features from API
await gb.loadFeatures();

// Set user attributes for targeting
gb.setAttributes({
  id: user.id,
  email: user.email,
  country: user.country,
  plan: user.plan,
  deviceType: isMobile ? 'mobile' : 'desktop',
});

// Provider component
function App() {
  return (
    
      
        
      
    
  );
}

// Use in components
function CheckoutPage() {
  // Boolean flag
  const showNewCheckout = useFeatureIsOn('new-checkout');

  // Multivariate experiment
  const buttonColor = useFeatureValue('checkout-button-color', 'blue');

  // JSON configuration
  const checkoutConfig = useFeatureValue('checkout-config', {
    showPromoCode: true,
    maxItems: 10
  });

  return (
    
{showNewCheckout ? ( ) : ( )}
); }

OpenFeature: Vendor-Agnostic Standard

OpenFeature is a CNCF sandbox project that provides a standardized API for feature flags, allowing you to switch between providers without rewriting code.

// Install: npm install @openfeature/server-sdk
// Plus your provider: npm install @openfeature/launchdarkly-provider

import { OpenFeature } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@openfeature/launchdarkly-provider';

// Set up provider (can be switched without code changes)
OpenFeature.setProvider(
  new LaunchDarklyProvider(process.env.LAUNCHDARKLY_SDK_KEY)
);

// Get client
const client = OpenFeature.getClient();

// Evaluate flags with standard API
const context = {
  targetingKey: 'user-123',
  email: 'alice@example.com',
  plan: 'premium'
};

// Boolean evaluation
const showNewDashboard = await client.getBooleanValue(
  'new-dashboard',
  false, // default
  context
);

// String evaluation
const theme = await client.getStringValue(
  'ui-theme',
  'light',
  context
);

// Number evaluation
const maxItems = await client.getNumberValue(
  'cart-max-items',
  10,
  context
);

// Object evaluation
const config = await client.getObjectValue(
  'feature-config',
  { enabled: false },
  context
);

// Get detailed evaluation with reason
const details = await client.getBooleanDetails(
  'new-dashboard',
  false,
  context
);

console.log(`Value: ${details.value}`);
console.log(`Reason: ${details.reason}`);
console.log(`Variant: ${details.variant}`);

Feature Flag Patterns

Percentage Rollout

// Gradual rollout pattern
const rolloutConfig = {
  'new-checkout': {
    enabled: true,
    percentage: 25, // 25% of users
    rules: [
      { segment: 'internal', percentage: 100 }, // All internal users
      { segment: 'beta', percentage: 100 },     // All beta users
      { segment: 'default', percentage: 25 }    // 25% of others
    ]
  }
};

// Typical rollout schedule:
// Day 1: 1% (catch critical bugs)
// Day 2: 10% (monitor metrics)
// Day 3: 25% (broader testing)
// Day 5: 50% (half of users)
// Day 7: 100% (full rollout)

User Segmentation

// Target specific user segments
const segmentRules = {
  'premium-dashboard': {
    rules: [
      {
        conditions: [
          { attribute: 'plan', operator: 'in', values: ['pro', 'enterprise'] }
        ],
        variation: true
      },
      {
        conditions: [],
        variation: false // Default for others
      }
    ]
  },
  'us-only-feature': {
    rules: [
      {
        conditions: [
          { attribute: 'country', operator: 'equals', value: 'US' }
        ],
        variation: true
      }
    ]
  }
};

Kill Switch Pattern

// Wrap risky operations with kill switches
async function processPayment(order) {
  // Kill switch for payment processing
  if (!featureFlags.isEnabled('payments-enabled')) {
    throw new ServiceUnavailableError(
      'Payment processing is temporarily disabled'
    );
  }

  // Circuit breaker for third-party service
  if (!featureFlags.isEnabled('stripe-integration')) {
    return fallbackPaymentProcessor(order);
  }

  return stripePaymentProcessor(order);
}

// Rate limiting flag
async function handleApiRequest(req) {
  const rateLimit = featureFlags.getValue('api-rate-limit', 100);
  
  if (await isRateLimited(req.userId, rateLimit)) {
    throw new TooManyRequestsError();
  }
  
  return processRequest(req);
}

Best Practices for Feature Flagging

Naming Conventions

// Use consistent naming patterns
const flagNamingExamples = {
  // Format: [scope].[feature].[variant]
  
  // Release flags (temporary)
  'release.new-checkout': true,
  'release.redesigned-dashboard': true,
  
  // Experiment flags (temporary)
  'experiment.checkout-button-color': 'blue',
  'experiment.pricing-page-layout': 'variant-b',
  
  // Ops flags (permanent)
  'ops.enable-caching': true,
  'ops.rate-limit-requests': 1000,
  
  // Permission flags (permanent)
  'permission.premium-features': false,
  'permission.api-access': true,
  
  // Bad examples (avoid)
  'newFeature': true,        // Not descriptive
  'test123': true,           // Meaningless
  'johns-experiment': true,  // Personal names
};

Flag Lifecycle Management

// Track flag metadata
const flagMetadata = {
  'release.new-checkout': {
    owner: 'checkout-team',
    createdAt: '2025-01-01',
    expiresAt: '2025-02-01', // Schedule removal
    ticket: 'JIRA-1234',
    description: 'New streamlined checkout flow',
    type: 'release',
    environments: ['development', 'staging', 'production']
  }
};

// Automated cleanup script
async function findStaleFlags() {
  const flags = await flagService.getAllFlags();
  const now = new Date();
  
  const staleFlags = flags.filter(flag => {
    if (!flag.expiresAt) return false;
    return new Date(flag.expiresAt) < now;
  });
  
  return staleFlags;
}

// Report stale flags in CI/CD
async function checkForStaleFlags() {
  const staleFlags = await findStaleFlags();
  
  if (staleFlags.length > 0) {
    console.warn('Found stale flags that should be removed:');
    staleFlags.forEach(flag => {
      console.warn(`  - ${flag.name} (expired: ${flag.expiresAt})`);
    });
  }
}

Testing with Feature Flags

// Unit tests should test both states
describe('Checkout', () => {
  it('renders new checkout when flag is enabled', () => {
    mockFlags({ 'new-checkout': true });
    
    render();
    
    expect(screen.getByTestId('new-checkout')).toBeInTheDocument();
  });

  it('renders legacy checkout when flag is disabled', () => {
    mockFlags({ 'new-checkout': false });
    
    render();
    
    expect(screen.getByTestId('legacy-checkout')).toBeInTheDocument();
  });
});

// Integration tests with flag overrides
describe('Checkout E2E', () => {
  it('completes purchase with new checkout', () => {
    cy.setFeatureFlags({ 'new-checkout': true });
    
    cy.visit('/checkout');
    cy.get('[data-testid="new-checkout"]').should('exist');
    cy.get('[data-testid="place-order"]').click();
    cy.url().should('include', '/confirmation');
  });
});

Common Mistakes to Avoid

Avoid these common pitfalls that lead to technical debt and operational issues.

1. Never Cleaning Up Flags

// ❌ Bad: Flags accumulate forever
// 200+ flags, most abandoned

// ✅ Good: Set expiration dates and review regularly
// - Remove release flags within 2 weeks of 100% rollout
// - Archive experiment flags after analysis complete
// - Review all flags monthly

2. No Default Values

// ❌ Bad: No fallback if service is down
const enabled = await ldClient.variation('new-feature', user);
// Returns undefined if service unavailable!

// ✅ Good: Always provide sensible defaults
const enabled = await ldClient.variation('new-feature', user, false);
// Returns false if service unavailable

3. Nesting Flags Too Deeply

// ❌ Bad: Complex nested conditions
if (flags.newDashboard) {
  if (flags.dashboardV2) {
    if (flags.dashboardV2Charts) {
      renderV2Charts();
    }
  }
}

// ✅ Good: Flat, independent flags
const chartVersion = flags.getValue('dashboard-charts-version', 'v1');
renderCharts(chartVersion);

4. Using Flags for Permanent Configuration

// ❌ Bad: Feature flag for permanent setting
const apiUrl = flags.getValue('api-url', 'https://api.example.com');

// ✅ Good: Use environment variables for configuration
const apiUrl = process.env.API_URL;

// Feature flags are for temporary toggles, not permanent config

Conclusion

Feature flagging gives teams a safer, smarter way to deploy software. With tools like LaunchDarkly for enterprise needs, Unleash and Flagsmith for self-hosted flexibility, GrowthBook for experimentation focus, or OpenFeature for vendor-agnostic implementations, you can ship features gradually, test safely with real users, and react instantly when issues arise.

The key to successful feature flagging is treating flags as temporary tools with clear ownership, expiration dates, and cleanup processes. Whether you choose a fully managed platform or an open-source alternative, feature flags help you reduce deployment risk and increase release velocity.

For related topics, explore Blue/Green vs Canary Deployments: When and How to Use Each to learn how flags support progressive delivery. For CI/CD pipeline integration, see CI/CD for Node.js Projects Using GitHub Actions. You can also visit the LaunchDarkly developer guide, the Unleash documentation, the Flagsmith documentation, and the OpenFeature specification for deeper exploration of feature flag implementations.

Leave a Comment