I’ve been thinking about how we build software lately. Specifically, how we release it. We’ve all been there: you deploy a new feature, hold your breath, and hope nothing breaks. What if there was a better way? What if you could test a feature with just 5% of your users before showing it to everyone? Or instantly turn off a problematic payment flow without rolling back your entire application? This is the power of a feature flag system. Let’s build one together.
Think of a feature flag as a simple on/off switch for parts of your application. It’s code that checks a condition before deciding which path to take. This simple concept changes everything. It lets you separate deployment from release. You can deploy code on Monday but wait until Friday to show it to users. You can run experiments, manage permissions, and create safety nets.
Why build your own? While there are great commercial services, creating your system offers complete control, cost savings, and a deep understanding of the mechanics. It also fits perfectly into your existing infrastructure. I want to guide you through building a production-ready system using Node.js and MongoDB. We’ll add real-time updates so changes reflect instantly, without restarting your apps.
Let’s start with the core idea. A feature flag service has two main jobs. First, it stores the rules (like “enable for users in the USA” or “show to 20% of traffic”). Second, it evaluates these rules for a specific request. We need this evaluation to be incredibly fast—think microseconds, not milliseconds. Every API call might check multiple flags, so speed is critical.
We’ll use a simple but powerful stack. Node.js and Express will handle our API. MongoDB will store our flag configurations because its flexible documents are perfect for complex rules. For real-time magic, we’ll use Server-Sent Events (SSE). It’s simpler than WebSockets and perfect for this one-way, server-to-client update flow.
Here’s a glimpse of what our flag data might look like in code. How do you think we should structure a rule that targets users by both location and subscription plan?
// Example flag configuration
const newDashboardFlag = {
name: "new_dashboard_ui",
enabled: true,
targeting: {
rules: [
{
attribute: "country",
operator: "IN",
value: ["US", "CA", "UK"]
},
{
attribute: "subscription_tier",
operator: "EQUALS",
value: "premium"
}
],
rollout_percentage: 30 // Enable for 30% of matching users
}
}
The heart of the system is the evaluation engine. It takes a flag configuration and a user’s context (like their user ID, country, or role) and returns a simple yes or no. The logic must handle different types of rules: exact matches, list checks, percentage rollouts, and even complex AND/OR conditions. We need to ensure this decision is consistent. The same user should get the same result every time during a session.
We achieve consistency with deterministic percentage rollouts. Instead of a random coin flip, we use a hash of the flag name and the user ID. This creates a predictable number between 0 and 100. If the flag is set for a 25% rollout, we enable it for users whose hash falls below 25. This way, the same user always gets the same result.
// Deterministic percentage rollout
function isInRolloutPercentage(flagName, userId, percentage) {
// Create a simple hash from flag name + userId
const hashString = `${flagName}:${userId}`;
let hash = 0;
for (let i = 0; i < hashString.length; i++) {
hash = ((hash << 5) - hash) + hashString.charCodeAt(i);
hash |= 0; // Convert to 32-bit integer
}
// Get a number between 0-99
const userBucket = Math.abs(hash) % 100;
return userBucket < percentage;
}
Now, let’s talk about real-time updates. When a product manager turns a flag on in an admin dashboard, we don’t want to wait. We want every connected application to know immediately. This is where Server-Sent Events shine. Our client applications open a long-lived HTTP connection to the server. When a flag changes, the server sends a message down this pipe. The client updates its local cache and uses the new rules instantly.
Setting up an SSE endpoint in Express is straightforward. The client connects, and we keep that connection open, sending data whenever we have an update. Can you imagine how this changes the workflow for your team? They gain immediate control.
// Server-Sent Events endpoint in Express
app.get('/api/flags/stream', (req, res) => {
// Set headers for SSE
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// Send a ping every 30 seconds to keep connection alive
const pingInterval = setInterval(() => {
res.write('event: ping\ndata: {}\n\n');
}, 30000);
// Listen for flag update events (from a message bus or database change stream)
flagUpdateEmitter.on('flag.updated', (flagData) => {
res.write(`event: flag-update\ndata: ${JSON.stringify(flagData)}\n\n`);
});
// Clean up on client disconnect
req.on('close', () => {
clearInterval(pingInterval);
flagUpdateEmitter.off('flag.updated'); // Remove listener
});
});
Performance is non-negotiable. We cannot query the database for every flag check. The solution is a two-layer cache. First, the main service holds all flags in memory. When a flag is updated, we invalidate this cache. Second, each client SDK has its own local cache. It gets the full set of rules on startup and listens for incremental updates via SSE. This means evaluation happens locally in the client, with zero network delay.
What about the database schema? We need to store flags, their targeting rules, and maybe some analytics on how often they’re checked. MongoDB’s document model is ideal. We can store complex nested rules directly. We’ll also add indexes on the flag name and update timestamps for fast queries.
Building an admin dashboard is where it all comes together. This React application lets non-technical team members manage flags. They can create a flag, set up targeting rules using a visual editor, and control the rollout percentage with a slider. They see which flags are active and who they’re affecting. This dashboard talks to our Node.js API to make changes.
Let’s not forget about the client SDK. This is a small library that applications include. It handles connecting to the SSE stream, caching flags locally, and providing a simple API for developers to check flags. Its job is to abstract away all the complexity.
// Example SDK usage in a web application
import { FeatureFlagClient } from '@our-company/feature-flags';
const client = new FeatureFlagClient({
apiKey: 'your-key',
baseUrl: 'https://flags.yourcompany.com'
});
// During app initialization
await client.initialize();
// In your component or API route
const userContext = { userId: '123', country: 'US', tier: 'premium' };
if (client.isEnabled('new_dashboard_ui', userContext)) {
// Render the new UI
} else {
// Render the old UI
}
Testing this system is crucial. We need unit tests for the evaluation logic. We need integration tests to ensure the API and SSE stream work. We also need to simulate high load to ensure the caching works under pressure. What happens when 10,000 clients connect simultaneously? Our system must handle it gracefully.
Finally, we must consider production. We’ll add monitoring to track how many evaluations happen per second. We’ll set up alerts if the cache fails to update. We’ll use environment-specific configurations, so flags in development don’t affect production. We’ll also plan for cleanup—archiving old flags that are no longer used to keep the system tidy.
Building this system teaches you more than just feature flags. You learn about real-time communication, caching strategies, and designing APIs for both machines and humans. You create a tool that gives your entire team superpowers: safer deployments, data-driven decisions, and instant control.
The journey from a simple if-statement to a full-scale control plane is fascinating. It starts with a need for safety and grows into a platform for innovation. You begin by wanting to avoid a bad release and end up enabling continuous experimentation. That’s the real value.
I encourage you to take these concepts and start building. Begin with a simple in-memory store, then add persistence, then real-time updates. Each step adds capability. Share what you create. I’d love to hear about your implementation challenges and successes. Let me know in the comments what kind of targeting rules are most important for your applications. If you found this guide helpful, please like and share it with other developers on this journey.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva