Feature flags -- also called feature toggles or feature switches -- are one of those techniques that sounds more complex than it is. The concept is simple: instead of deciding at deploy time whether code runs for all users, you add a runtime check that lets you turn functionality on or off for specific users, percentages of traffic, or everyone at once.
The practical benefit is significant. You can ship code to production without activating it, gradually roll out to a percentage of users, roll back instantly if something goes wrong, and test new UI on internal users before releasing broadly. None of this requires a third-party SaaS platform. A basic implementation takes a few hours and gives you most of the control you will actually need.
This is a practical guide to building a feature flag system from scratch. We will cover the data model, a server-side evaluator, a client-side hook, and the operational habits that keep flags from becoming a maintenance burden.

Photo by JESHOOTS-com on Pixabay
Why Feature Flags Matter for App Deployment
The standard deployment process has a binary outcome: code is either live for everyone or not deployed at all. This creates two common problems.
First, it concentrates risk at deployment time. Every deploy is either a complete success or a complete failure. If a bug appears after release, the only options are to push a hotfix or roll back the entire deploy.
Second, it limits your ability to test. Getting real-world feedback on a new feature requires shipping it to production, which means all users see it at once.
Feature flags solve both problems. You deploy the code whenever it is ready, but you control who sees the new behavior. A bug in a flagged feature affects only the users who have it enabled -- usually 1 to 10 percent of traffic in an early rollout -- and you can disable it instantly without a code change or redeploy.
This technique, often called progressive delivery in modern deployment literature, is standard practice at companies that ship frequently. It does not require a sophisticated infrastructure to implement -- a simple database table and a few dozen lines of code cover most production use cases.
The Core Data Model
A feature flag system needs to store three things for each flag:
- A unique key that your code references
- Whether the flag is currently enabled
- Optionally, a rollout percentage (for gradual releases)
A PostgreSQL table handles this cleanly:
CREATE TABLE feature_flags (
id SERIAL PRIMARY KEY,
flag_key VARCHAR(100) UNIQUE NOT NULL,
is_enabled BOOLEAN DEFAULT false,
rollout_pct SMALLINT DEFAULT 100 CHECK (rollout_pct BETWEEN 0 AND 100),
description TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
For a small application, this table can live in your existing application database. For larger systems with many flags and frequent evaluations, you might cache the entire flags table in Redis with a short TTL to avoid a database read on every request.
The rollout_pct column lets you release to a percentage of users. When is_enabled = true and rollout_pct = 10, the flag evaluates as active for roughly 10 percent of requests.

Photo by Christina Morillo on Pexels
Building a Server-Side Evaluator
The flag evaluator is the code that decides, for a given request, whether a flag is active. Keep it simple:
// lib/flags.js (Node.js / Express)
const db = require('./db');
// Simple deterministic hash -- distributes users consistently
function stableHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) >>> 0;
}
return hash % 100;
}
async function isEnabled(flagKey, userId = null) {
const result = await db.query(
'SELECT is_enabled, rollout_pct FROM feature_flags WHERE flag_key = $1',
[flagKey]
);
if (!result.rows.length) return false;
const { is_enabled, rollout_pct } = result.rows[0];
if (!is_enabled) return false;
if (rollout_pct >= 100) return true;
// Use flagKey + userId for stable per-user assignment
const bucket = stableHash(`${flagKey}:${userId ?? 'anon'}`);
return bucket < rollout_pct;
}
module.exports = { isEnabled };
A few implementation notes:
The stableHash function matters. Without a deterministic hash, the same user might see different behavior on successive requests during a partial rollout. The hash of flagKey + userId ensures that user 12345 always gets the same flag state for a given flag key, regardless of how many times the function is called.
Default to false on missing flags. If the flag key does not exist in the database, isEnabled returns false. This is the safe default -- code behind a missing flag stays inactive rather than accidentally activating.
Avoid N+1 queries. If your request handler checks multiple flags, consider fetching all flags for the current environment in one query and caching the result in request context for the duration of the request.
Adding a Client-Side React Hook
For web applications using React, the cleanest client-side pattern is a hook that fetches flag state from a dedicated API endpoint:
// hooks/useFeatureFlag.js
import { useState, useEffect } from 'react';
export function useFeatureFlag(flagKey) {
const [enabled, setEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
fetch(`/api/flags/${flagKey}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setEnabled(data.enabled ?? false);
})
.catch(() => {
if (!cancelled) setEnabled(false);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, [flagKey]);
return { enabled, loading };
}
Usage in a component:
function NewDashboard() {
const { enabled, loading } = useFeatureFlag('new_dashboard_v2');
if (loading) return <Spinner />;
return enabled ? <DashboardV2 /> : <DashboardV1 />;
}
The cancelled flag in the effect prevents state updates after the component unmounts, which avoids a common React memory leak warning. The default value of false ensures the old behavior shows while the flag state loads and if the API call fails.

Photo by Lucas Mota on Pexels
Managing Flag Lifecycle Without an Operations Nightmare
The most common complaint about feature flag systems is that they accumulate permanent technical debt. Flags that were supposed to be temporary live in the codebase for years, their original purpose forgotten. Here is how to prevent that:
Name flags with lifecycle intent. Use a prefix convention: temp_ for flags meant to ship and delete within 30 days, exp_ for longer-running experiments, ops_ for permanent operational toggles (like maintenance mode or feature gating). The prefix makes it obvious at code review time what the expected lifecycle is.
Set a deletion date at creation. When you add a flag to the database, add a scheduled_cleanup_date column and set it at creation time. A weekly SQL query that returns all flags past their cleanup date gives you an automatic review queue.
Remove code, not just flags. When a flag is retired, delete both the database row and all the conditional logic in the application code. A dead branch around a deleted flag is not harmful, but it adds cognitive overhead. The full cleanup is the right cleanup.
"The most expensive part of a feature flag isn't the code, it's the decision debt. Flags that sit in the codebase with no clear owner or expiry date start costing more to maintain than they ever saved in deployment risk." - Dennis Traina, founder of 137Foundry
When to Add More Complexity
The system described above handles most production use cases: simple on/off flags, percentage rollouts, and per-user bucketing. A few scenarios that justify additional complexity:
User targeting: Rather than hash-based percentage rollouts, you may want to target flags at specific user segments (beta users, users in a specific region, users on a specific subscription tier). This requires storing targeting rules alongside the flag definition and a more complex evaluator. It is worth building if you regularly need to target specific cohorts rather than random percentages.
Real-time updates without redeploy: The database-backed approach described here requires either a cache TTL or a page refresh for flag changes to take effect. If you need instant propagation to all connected clients -- for example, disabling a feature in response to an incident -- a WebSocket or server-sent events approach lets you push flag state changes without a cache flush or reload.
Audit log: For compliance-sensitive applications, log every flag change (who changed it, from what value, to what value, at what time) in a separate audit table. This is a small addition and pays for itself quickly when you need to reconstruct what was enabled during a production incident.
For 137Foundry's web development clients, we typically start with the simple database-backed version and add complexity only when a specific operational need justifies it. Most applications never need more than flag key, enabled state, and rollout percentage. The 137Foundry services page covers how we approach infrastructure decisions like this in the context of a broader application build.

Photo by Mizuno K on Pexels
Summary
A working feature flag system for most web applications requires:
- A
feature_flagsdatabase table with key, enabled state, and rollout percentage - A server-side evaluator with stable hash-based bucketing
- A client-side hook or context provider that fetches flag state
- Naming conventions and cleanup policies to prevent accumulation
The full implementation is under 100 lines of production code. The operational benefit -- the ability to ship to production without activating, roll back without redeploy, and test on a small percentage of users -- is available from the first flag you deploy.
The application development team at 137Foundry builds this kind of infrastructure into most new projects we take on. If you are starting a new web application or adding progressive delivery to an existing one, the pattern described here is a solid foundation. For teams that eventually need more than a homegrown solution, LaunchDarkly is the market leader in SaaS feature flag management and worth evaluating once your flag count makes a managed service cost-effective.