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 ☕