Feature Toggles

A Complete Guide to Managing Feature Releases

Feature toggles (also known as feature flags) are a powerful technique that allows teams to modify system behavior without changing code. They enable separating code deployment from feature releases — a critical capability for Continuous Delivery.

This article is inspired by Pete Hodgson's material on martinfowler.com and supplemented with practical experience of using toggles in production systems.

Why Feature Toggles

Imagine this scenario: a team is working on a new algorithm in a critical part of the system. The changes will take several weeks. The traditional approach is to create a feature branch and merge it after completion. But long-lived branches lead to painful merge conflicts.

The alternative is to work in trunk (main branch), hiding incomplete functionality behind a feature toggle:

function reticulateSplines() {
  if (featureIsEnabled("use-new-algorithm")) {
    return enhancedSplineReticulation();
  } else {
    return oldFashionedSplineReticulation();
  }
}

Now the code can be deployed to production every day, while the new algorithm remains hidden from users until it's fully ready. This is the key principle of Continuous Delivery: separating code deployment from feature release.

Four Categories of Toggles

Not all toggles are created equal. They differ along two key dimensions: longevity (how long the toggle will exist) and dynamism (how often the toggle decision changes).

Release Toggles

Allow deploying incomplete code to production as hidden (latent) functionality.

Example: A new "Delivery Date Estimation" feature is implemented for only one shipping partner. The product manager wants to wait until all partners are integrated before release. The feature is hidden behind a release toggle.

Rule: A release toggle should be removed within 1-2 weeks after the feature is enabled for all users.

Experiment Toggles

Used for A/B testing. Each user is assigned to a cohort and sees the corresponding variant of functionality.

Example: The team debates whether a new recommendation algorithm will increase conversion. Instead of arguing, they launch an A/B test: 50% of users see the old algorithm, 50% see the new one. After two weeks, the data shows which variant performs better.

// Cohort assignment based on user ID
function getUserCohort(userId, experimentName) {
  const hash = hashCode(userId + experimentName);
  return hash % 100 < 50 ? 'control' : 'treatment';
}

Ops Toggles

Give operators the ability to quickly change system behavior in production. Often called "kill switches."

Example: Under high load, you can disable the heavy recommendations panel on the homepage. This preserves availability of core functionality.

Important: Ops toggles must be switchable without deployment. If changing a toggle requires deployment — it's not an ops toggle.

Permission Toggles

Control access to features for specific user groups: premium subscribers, beta testers, internal users.

Example: "Champagne Brunch" — a practice where new features are first available only to company employees (dogfooding), then to beta users, and only then to everyone.

function canAccessFeature(user, featureName) {
  if (user.isEmployee) return true;
  if (user.isBetaTester && betaFeatures.includes(featureName)) return true;
  if (user.isPremium && premiumFeatures.includes(featureName)) return true;
  return generallyAvailableFeatures.includes(featureName);
}

Implementation Techniques

Feature toggles can turn code into spaghetti if you scatter if/else checks throughout the codebase. Here are techniques to avoid this.

Separate Decision Point from Logic

Bad — toggle logic scattered through code:

// Bad: magic string and direct config access
function generateInvoiceEmail() {
  const baseEmail = buildEmailForInvoice(this.invoice);
  if (features.isEnabled("next-gen-ecomm")) {
    return addOrderCancellationContent(baseEmail);
  }
  return baseEmail;
}

Better — encapsulated decisions:

// Good: decisions in one place
const featureDecisions = {
  includeOrderCancellationInEmail() {
    return features.isEnabled("next-gen-ecomm");
  }
};

function generateInvoiceEmail() {
  const baseEmail = buildEmailForInvoice(this.invoice);
  if (featureDecisions.includeOrderCancellationInEmail()) {
    return addOrderCancellationContent(baseEmail);
  }
  return baseEmail;
}

Inversion of Control (IoC)

Even better — pass the decision through configuration:

function createInvoiceEmailer(config) {
  return {
    generateInvoiceEmail() {
      const baseEmail = buildEmailForInvoice(this.invoice);
      if (config.includeOrderCancellation) {
        return addOrderCancellationContent(baseEmail);
      }
      return baseEmail;
    }
  };
}

// Feature-aware factory
function createFeatureAwareFactory(featureDecisions) {
  return {
    invoiceEmailer() {
      return createInvoiceEmailer({
        includeOrderCancellation: featureDecisions.includeOrderCancellationInEmail()
      });
    }
  };
}

Now InvoiceEmailer doesn't know toggles exist. It's easy to test in isolation.

Strategy Pattern

For long-lived toggles or multiple toggle points, use the Strategy pattern:

function createInvoiceEmailer(contentEnhancer) {
  return {
    generateInvoiceEmail() {
      const baseEmail = buildEmailForInvoice(this.invoice);
      return contentEnhancer(baseEmail);
    }
  };
}

// Strategies
const addOrderCancellation = (email) => addOrderCancellationContent(email);
const noOp = (email) => email;

// Strategy selection at creation time
const emailer = featureDecisions.includeOrderCancellationInEmail()
  ? createInvoiceEmailer(addOrderCancellation)
  : createInvoiceEmailer(noOp);

Toggle Configuration

There's a spectrum of approaches for storing configuration — from simple to complex:

Approach Description When to Use
Hardcoded Commenting/uncommenting code Local development only
Environment variables Env vars Release toggles, simple cases
Config file JSON/YAML file in repository Release toggles with versioning
Database DB table + admin UI Dynamic toggles, A/B tests
Distributed config Consul, etcd, Zookeeper Ops toggles, microservices
Feature flag service LaunchDarkly, Unleash, Flipper All categories, teams without DevOps
Recommendation: Start simple (environment variables or config file). Move to more complex solutions only when there's a real need.

Lifecycle Management

Toggles tend to accumulate. Each toggle adds conditional logic, increases testing scope, and cognitive load. Without discipline, the system becomes a maze.

Management Practices

Testing Systems with Toggles

Minimum set of configurations to test:

  1. Configuration expected in production (all new features enabled)
  2. Fallback configuration (all new features disabled)
  3. Configuration with all toggles enabled (if applicable)

Don't test every combination — that's combinatorial explosion. Most toggles don't interact with each other.

Summary

Feature toggles are not just if/else. They're a tool that requires understanding and discipline.

Category Lifespan Dynamism Managed By
Release Days to weeks Static Developers
Experiment Weeks Per-request Product, analysts
Ops Days to years Static, fast switching Ops, SRE
Permission Years Per-request Product, business

Key principles: