DEV Community

Cover image for Managing Feature Flag Versions and Migrations in JavaScript
HexShift
HexShift

Posted on

Managing Feature Flag Versions and Migrations in JavaScript

Feature flags aren’t just about toggles — they evolve over time. A flag might start as a boolean, later switch to an expression, and eventually get removed. Without a versioning strategy, things break: stale configs, incompatible logic, and orphaned code paths.

In this article, you’ll learn how to build a versioned feature flag system in JavaScript that can safely evolve — without breaking your app.


Step 1: Version Your Flags Explicitly

Start by defining each flag with a version field:


const fallbackFlags = {
  newUI: {
    version: 1,
    expr: "user.plan === 'pro'",
  },
  searchBoost: {
    version: 2,
    expr: "getBucket(user.id) < 30",
  },
};

This allows your app to know what format or logic it's dealing with.


Step 2: Handle Version Migrations at Load Time

When loading remote or cached flags, normalize and upgrade them if needed:


function migrateFlag(flag, currentVersion) {
  const upgraded = { ...flag };

  if (!flag.version || flag.version < currentVersion) {
    // Example: older flags used plain booleans
    if (typeof flag === "boolean") {
      upgraded.expr = flag ? "true" : "false";
    }

    upgraded.version = currentVersion;
  }

  return upgraded;
}

Wrap this into your loading logic so all flags are upgraded before evaluation.


Step 3: Avoid Breaking Changes by Supporting Legacy Versions

If your app may receive old snapshots (e.g. from offline clients), support evaluation of multiple versions:


function evaluateFlag(flag, user, context = {}) {
  try {
    if (flag.version === 1) {
      const fn = new Function('user', `return (${flag.expr})`);
      return !!fn(user);
    }

    if (flag.version === 2) {
      const fn = new Function('user', 'context', 'getBucket', `return (${flag.expr})`);
      return !!fn(user, context, getBucket);
    }

    return false;
  } catch {
    return false;
  }
}

You can remove support for old versions gradually, once they’re no longer needed.


Step 4: Track Flag Cleanup with Metadata

Add optional metadata to track rollout state:


{
  searchBoost: {
    version: 2,
    expr: "getBucket(user.id) < 30",
    deprecated: false,
    rolloutStatus: "active"
  }
}

This helps you audit flags later and safely remove them when they're no longer needed.


Step 5: Persist and Load Flag Snapshots with Version Checks

When storing snapshots (e.g. in localStorage or IndexedDB), include a schema or timestamp:


function storeFlags(flags) {
  localStorage.setItem('flagSnapshot', JSON.stringify({
    schemaVersion: 2,
    timestamp: Date.now(),
    flags,
  }));
}

This ensures you don’t accidentally load outdated or incompatible formats later.


Pros:

  • 🔢 Explicit flag versions prevent runtime errors
  • 🔄 Allows smooth migrations as logic evolves
  • 📦 Supports old clients without breaking new features
  • ✅ Easy to audit and clean up stale flags

⚠️ Cons:

  • 🛠 Slightly more complexity to manage versions
  • 📉 Can bloat your config if you don’t clean up
  • 🧹 Requires discipline to deprecate and remove old logic

Summary

A versioned feature flag system keeps your app flexible without sacrificing safety. By migrating flags, evaluating based on version, and tracking metadata, you can build a flag system that evolves alongside your product — and never breaks during rollout.


Want to learn much more? My full guide explains in detail how to:

  • Use JavaScript expressions for safe feature flag evaluation
  • Handle gradual feature rollouts and exposure
  • Implement flag versioning, migration strategies, and more
  • Design a feature flagging system that works offline and is resilient to failure

Feature Flag Engineering Like a Pro: From JS Expressions to Global Rollouts — just $10 on Gumroad.

If this was helpful, you can also support me here: Buy Me a Coffee

Top comments (0)