Feature flags let me control feature releases without redeploying code. They act as switches that turn functionalities on or off for specific user groups. I’ve found them invaluable for reducing risk, enabling testing in production, and responding quickly to issues.
Managing flags requires careful organization. I maintain a centralized system - either a configuration file or remote service - to control flag states. This avoids scattering conditionals throughout the codebase. For Node.js backends, I use middleware like this:
// Feature flag service with user segmentation
class FeatureFlagService {
constructor(flags) {
this.flags = flags;
}
isEnabled(flagName, user) {
const flag = this.flags[flagName];
if (!flag || !flag.enabled) return false;
// Percentage-based rollout
if (flag.rolloutPercentage) {
const userIdHash = this._hashUserId(user.id);
return userIdHash % 100 < flag.rolloutPercentage;
}
// Attribute-based segmentation
if (flag.segments) {
return flag.segments.some(segment =>
segment.country && user.country === segment.country ||
segment.plan && user.subscriptionPlan === segment.plan
);
}
return true;
}
_hashUserId(userId) {
return [...userId].reduce((sum, char) => sum + char.charCodeAt(0), 0) % 100;
}
}
// Implementation
const flags = {
darkMode: {
enabled: true,
rolloutPercentage: 25 // 25% of users
},
premiumFeatures: {
enabled: true,
segments: [{ plan: 'premium' }, { country: 'US' }]
}
};
const featureService = new FeatureFlagService(flags);
const user = { id: 'user123', country: 'US', subscriptionPlan: 'basic' };
console.log(featureService.isEnabled('darkMode', user)); // Random 25% chance
console.log(featureService.isEnabled('premiumFeatures', user)); // True for US users
For frontends, I create reusable components. This React implementation includes analytics hooks to track feature exposure:
// FeatureToggle component with analytics
import { useEffect, useContext } from 'react';
import { AnalyticsContext } from './Analytics';
import FeatureFlagContext from './FeatureFlagContext';
const FeatureToggle = ({ flag, children, fallback = null }) => {
const { isEnabled } = useContext(FeatureFlagContext);
const { trackEvent } = useContext(AnalyticsContext);
useEffect(() => {
if (isEnabled(flag)) {
trackEvent('feature_exposure', { flag_name: flag });
}
}, [flag, isEnabled, trackEvent]);
return isEnabled(flag) ? children : fallback;
};
// Implementation in App.jsx
const App = () => (
<FeatureFlagProvider flags={flags}>
<AnalyticsProvider>
<FeatureToggle flag="new_ui" fallback={<OldDashboard />}>
<NewDashboard />
</FeatureToggle>
<FeatureToggle flag="beta_features">
<BetaSection />
</FeatureToggle>
</AnalyticsProvider>
</FeatureFlagProvider>
);
Progressive rollouts follow a phased approach. I start with internal teams, then expand to 5% of users, gradually increasing to 100% over days. The key is monitoring metrics at each stage - if error rates spike, I pause the rollout.
Kill switches save me during incidents. When our checkout service recently had a pricing bug, I disabled it instantly with a flag:
// Emergency kill switch
app.post('/checkout', (req, res) => {
if (!featureService.isEnabled('checkout_v2', req.user)) {
return res.status(503).json({ error: 'Service unavailable' });
}
// Process payment
});
For A/B testing, I combine flags with analytics. When testing two checkout flows, I assign users to variants using consistent hashing and track conversion rates:
// A/B test allocation
function getVariant(userId, testName) {
const tests = {
checkout_ui: ['v1', 'v2'] // 50/50 split
};
const hash = [...userId].reduce((sum, c) => sum + c.charCodeAt(0), 0);
const index = hash % tests[testName].length;
return tests[testName][index];
}
// Usage
const variant = getVariant(user.id, 'checkout_ui');
trackEvent('checkout_rendered', { variant });
return variant === 'v1' ? <CheckoutV1 /> : <CheckoutV2 />;
To prevent technical debt, I enforce expiration dates for flags and create cleanup tasks. Each flag includes metadata like this:
{
new_search: {
enabled: true,
created: '2023-06-01',
owner: '[email protected]',
expires: '2023-12-01' // Automatic reminders sent 30 days pre-expiry
}
}
For complex systems, I use distributed caching to maintain flag consistency across servers. Redis works well for this:
// Distributed flag cache with fallback
async function getFeatureFlags() {
try {
const cachedFlags = await redis.get('feature-flags');
if (cachedFlags) return JSON.parse(cachedFlags);
} catch (e) {
console.error('Cache fetch failed', e);
}
// Fallback to database
return db.query('SELECT * FROM feature_flags');
}
// Refresh cache every 60 seconds
setInterval(async () => {
const flags = await db.query('SELECT * FROM feature_flags');
await redis.set('feature-flags', JSON.stringify(flags));
}, 60000);
Feature flags changed how I deploy software. I now release code hidden behind disabled flags, then activate features when ready. This separates deployment from release, reducing Friday-afternoon anxiety. When things go wrong, I toggle features off instead of rolling back entire deployments.
Maintaining flag hygiene matters. I audit flags monthly, remove stale ones, and document each flag’s purpose. Teams review flags during sprint planning - if a flag’s been on for six months, we remove the toggle and delete the old code path. This discipline keeps systems clean while preserving deployment flexibility.