
Tenant Data Isolation: Patterns and Anti-Patterns
Explore effective patterns and pitfalls of tenant data isolation in multi-tenant systems to enhance security and compliance.
Jul 30, 2025
Read More
Feature flags (also called feature toggles) are one of the most powerful tools in a SaaS engineering team's arsenal. They let you deploy code to production without immediately exposing it to users, roll out features gradually to specific user segments, run A/B tests, and kill problematic features instantly without redeploying.
But feature flags introduce complexity. Poor implementation leads to technical debt, performance issues, and confusion. In this guide, we'll cover how to implement feature flags properly — from architecture decisions to operational best practices.
In traditional deployment, code merge = production release. If a feature breaks, you must revert the commit, redeploy, and wait for CI/CD to complete. With feature flags, you simply flip a switch and the feature disappears — instantly.
Key use cases:
At its core, a feature flag is a conditional statement:
if (featureFlags.isEnabled('new-dashboard', user)) {
return ;
} else {
return ;
}
But production-grade feature flag systems need more:
| Solution | Best For | Pricing |
|---|---|---|
| LaunchDarkly | Enterprise teams, complex targeting | Starts at $10/seat/month |
| Split.io | A/B testing focus | Starts at $33/month |
| Flagsmith | Open-source option, self-hostable | Free tier, then $45/month |
| Unleash | Developer-first, self-hostable | Open-source or hosted |
When to buy: If you need advanced targeting, A/B testing analytics, compliance features (audit logs, RBAC), or want to move fast. Commercial solutions provide SDKs, dashboards, and support out of the box.
When to build: If your needs are simple (basic on/off toggles), you want full control, or you're cost-sensitive (early-stage startup).
A minimal feature flag system needs:
Implementation time: 2-4 weeks for a basic system. However, you'll spend ongoing time maintaining it and adding features like percentage rollouts, scheduling, and audit logs.
Evaluate flags on the backend:
// Express middleware example
app.use(async (req, res, next) => {
const user = req.user;
req.featureFlags = await flagService.getFlagsForUser(user.id, user.orgId);
next();
});
app.get('/dashboard', (req, res) => {
if (req.featureFlags.isEnabled('new-dashboard')) {
res.json({ version: 'v2', features: [...] });
} else {
res.json({ version: 'v1', features: [...] });
}
});
Pros: Secure (flags not exposed to client), easier to manage, consistent evaluation
Cons: Adds latency to API calls unless cached properly
Evaluate flags in the browser/mobile app:
// React example
import { useFeatureFlag } from './featureFlags';
function Dashboard() {
const showNewDashboard = useFeatureFlag('new-dashboard');
return showNewDashboard ? : ;
}
Pros: No backend latency, instant UI updates when flags change
Cons: Flags are visible in network requests (users can see upcoming features), harder to keep flags in sync
Best practice: Use server-side for business logic and security-sensitive features. Use client-side for UI experiments and non-critical features.
Basic on/off toggles are just the start. Production feature flag systems support complex targeting:
flag: new-dashboard
enabled: true
rules:
- name: "Internal team"
condition: user.email endsWith "@company.com"
enabled: true
- name: "Beta customers"
condition: user.orgId in ["org-123", "org-456"]
enabled: true
- name: "Gradual rollout"
condition: hash(user.id) % 100 < 25
enabled: true
This enables the flag for:
For gradual rollouts, use deterministic hashing to ensure consistency:
function isInRollout(userId, flagName, percentage) {
const hash = murmurhash(`${flagName}:${userId}`);
return (hash % 100) < percentage;
}
This ensures:
Feature flag evaluation happens on every request. Poor implementation can add 50-100ms latency. Here's how to keep it fast:
class FeatureFlagService {
constructor() {
this.localCache = new Map();
this.lastRefresh = null;
}
async isEnabled(flagName, user) {
// Refresh cache if stale (> 60 seconds)
if (!this.lastRefresh || Date.now() - this.lastRefresh > 60000) {
await this.refreshCache();
}
const flag = this.localCache.get(flagName);
if (!flag) return false;
return this.evaluateRules(flag, user);
}
}
Instead of checking flags individually, evaluate all flags for a user once per request:
// Bad: Multiple flag checks
if (flags.isEnabled('feature-a')) { ... }
if (flags.isEnabled('feature-b')) { ... }
if (flags.isEnabled('feature-c')) { ... }
// Good: Batch evaluation
const userFlags = await flags.getAllForUser(user);
if (userFlags['feature-a']) { ... }
if (userFlags['feature-b']) { ... }
Feature flags are meant to be temporary. Permanent flags create technical debt — the codebase becomes littered with conditionals that are always true or always false.
| Type | Lifespan | Example |
|---|---|---|
| Release flags | 2-4 weeks | Gradual rollout of new feature |
| Experiment flags | 1-3 months | A/B test for checkout flow |
| Operational flags | Permanent | Kill switch for expensive API |
| Permission flags | Permanent | Enterprise-only features |
Automation tip: Use static analysis to find unused flags in your codebase automatically.
Feature flags complicate testing — you must test both states of every flag. Strategies:
describe('Dashboard', () => {
it('shows new dashboard when flag enabled', () => {
mockFlags({ 'new-dashboard': true });
render( );
expect(screen.getByText('Welcome to v2')).toBeInTheDocument();
});
it('shows old dashboard when flag disabled', () => {
mockFlags({ 'new-dashboard': false });
render( );
expect(screen.getByText('Welcome to v1')).toBeInTheDocument();
});
});
Test the most important flag combinations:
Don't test every permutation — that's exponential. Focus on realistic combinations.
More than 50 active flags suggests poor cleanup. Aim to remove flags within 2-4 weeks of full rollout. Only operational and permission flags should be permanent. Use automated reminders to prompt flag removal after rollout completion.
Use a database (or Redis) for flags that need to change dynamically without redeployment. Use config files only for environment-specific settings (like API keys). Database storage allows non-engineers to toggle flags via admin UI.
Set expiration dates when creating flags. Implement automated alerts when flags exceed their expected lifespan. Require approval for permanent flags. Include flag cleanup in your definition of done for feature releases.
Yes, if flag state changes mid-request. Cache flags per request to ensure consistency. If a user starts a checkout with the old flow, complete the entire flow with the old code — don't switch mid-transaction.
Use backward-compatible migrations. First deploy the migration (both old and new code work). Then enable the flag. Finally, remove old code after 100% rollout. This allows safe rollback at any point.
Need an expert team to provide digital solutions for your business?
Book A Free CallDive into a wealth of knowledge with our unique articles and resources. Stay informed about the latest trends and best practices in the tech industry.
View All articlesTell us about your vision. We'll respond within 24 hours with a free AI-powered estimate.
© 2026 Propelius Technologies. All rights reserved.