programming

How Feature Toggles Let You Ship Code Without Breaking Production

Learn how feature toggles let you deploy code safely, run gradual rollouts, and kill bugs instantly—without a full redeployment. Start shipping with confidence.

How Feature Toggles Let You Ship Code Without Breaking Production

Feature toggles are a way to turn code on and off without shipping a new version. I first learned about them when I broke production by pushing a half-finished feature. A senior engineer showed me how to wrap that feature in a simple if statement that checked a config file. That single trick changed how I thought about deployment.

At its core, a feature toggle is just a conditional check at runtime. For a small project you might store flags in a dictionary. Here is how I started:

class FeatureFlags:
    def __init__(self):
        self._flags = {
            "new_checkout_flow": False,
            "advanced_search": True,
            "beta_recommendations": False
        }
    
    def is_enabled(self, flag):
        return self._flags.get(flag, False)

flags = FeatureFlags()

def process_checkout(cart):
    if flags.is_enabled("new_checkout_flow"):
        return new_checkout_flow(cart)
    return legacy_checkout_flow(cart)

That worked until I needed more than on/off. I wanted to roll out a feature to ten percent of users first. A simple boolean could not do that. So I built a gradual rollout that used a hash of the user ID.

class GradualRollout {
    constructor(featureName, percentage = 0) {
        this.featureName = featureName;
        this.percentage = percentage;
    }
    
    isEnabled(userId) {
        const hash = this._hash(`${this.featureName}:${userId}`);
        return hash < this.percentage;
    }
    
    _hash(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            const char = str.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash |= 0;
        }
        return Math.abs(hash) % 100;
    }
}

const checkoutRollout = new GradualRollout("new_checkout", 10);

The hash made sure the same user always saw the same version. That kept the experience predictable. I increased the percentage every few days until it reached one hundred. When I felt confident, I removed the flag and the old code path.

Storing flags in code or environment variables worked for a while, but I wanted to change flags without redeploying. I moved the configuration to a database. That way I could flip a switch in a dashboard and the change would take effect immediately. Here is how I did it in Java with a cache to avoid hitting the database on every request:

@Service
public class FeatureToggleService {
    private final FeatureFlagRepository repository;
    private final Cache<String, Boolean> cache;
    
    public boolean isEnabled(String feature, User user) {
        String key = feature + ":" + user.getId();
        Boolean cached = cache.get(key);
        if (cached != null) return cached;
        
        FeatureFlag flag = repository.findByName(feature);
        boolean enabled = flag != null && 
            (flag.isGlobal() || 
             flag.getUserIds().contains(user.getId()) ||
             (flag.getPercentage() > 0 && 
              user.getId() % 100 < flag.getPercentage()));
        
        cache.put(key, enabled, Duration.ofMinutes(5));
        return enabled;
    }
}

I also added targeted rules: allow only users in a certain region or with a specific account tier. The database made that easy. I could even do A/B testing by splitting users randomly and showing them different versions of a feature.

Feature toggles are not free. They add complexity. The biggest problem I faced was forgetting to remove a toggle after the feature shipped. Old checks piled up and became dead code. I started using a script to list all toggles in the codebase and their usage frequency.

grep -r "is_enabled\|isFeatureEnabled" src/ | \
  awk -F"[" \]"{print $2}" | \
  sort | uniq -c | \
  sort -rn

If a toggle appeared in the code but was always on, I knew it was time to delete the check and the legacy code. I also set a reminder in my calendar two weeks after a full rollout: remove the flag.

Testing toggled code requires running tests with both states. In Python, I used pytest parametrization:

import pytest

@pytest.mark.parametrize("use_new_flow", [True, False])
def test_checkout_flow(use_new_flow):
    with mock.patch.object(FeatureFlags, 'is_enabled', 
                           return_value=use_new_flow):
        result = process_checkout(mock_cart())
        if use_new_flow:
            assert result == expected_new_result
        else:
            assert result == expected_legacy_result

That gave me confidence that both paths worked. I also added integration tests that ran against a real database with toggles on and off.

A common mistake is to nest toggles. If you have two flags that depend on each other, the combinations multiply and become impossible to test. I learned to keep each flag independent. If a feature required three separate flags, I grouped them under a single parent flag.

Logging toggle state helped me debug production issues. When something broke, I wanted to know which flags were active for the user. I added a middleware that attached the active flags to the request log.

func FeatureFlagMiddleware(featureFlags FeatureFlagService) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userID := r.Context().Value("userID").(string)
            activeFlags := featureFlags.ActiveFlagsForUser(userID)
            
            log.Printf("Request from user %s, active flags: %v", userID, activeFlags)
            
            ctx := context.WithValue(r.Context(), "flags", activeFlags)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

That log line saved me hours when a user reported an issue. I could see exactly what they were seeing.

I used feature toggles to enable trunk-based development. Instead of working on a feature branch for weeks, I merged unfinished code behind a toggle. My team could deploy the main branch anytime. Merge conflicts almost disappeared because everyone integrated often.

The performance cost of a toggle is tiny. A hash or a database read per request is nothing compared to rendering a page or querying a database. But I made sure not to put toggle checks inside tight loops. In one case, I accidentally called the flag service inside a loop that ran ten thousand times per request. That degraded response times by fifty milliseconds. I moved the check outside the loop and cached the result.

I also learned to separate feature toggles from configuration. Toggles control which version of a feature runs. Configuration controls parameters like timeouts or URLs. Mixing them leads to confusion. I kept toggles in one place and configuration in another.

The discipline of feature toggles is not only for developers. Product managers began using the dashboard to control who saw new features. They could turn on a feature for a specific customer who requested it. Support teams could disable a feature instantly if a bug was reported. The toggle became a tool for the whole organization.

Remember to eventually remove the toggle. Toggles are temporary by design. When a feature is fully rolled out and stable, the old code path should be deleted. The toggle check should be replaced with just the new code. I wrote a checklist for every feature:

  • Use a toggle to hide the new code.
  • Merge early and often.
  • Increase rollout percentage gradually.
  • Monitor for errors and performance changes.
  • When comfortable, set the toggle to 100%.
  • After two weeks, delete the toggle and the old code.

Neglecting the last step turns your codebase into a maze of conditional branches. I have seen projects with hundreds of toggles that nobody dared to remove. Every change required checking which flags were still active. The code became hard to read and harder to test.

Feature flags also allow for experimentation. I ran A/B tests by randomly assigning users to a control group and a treatment group. The flag system recorded which group each user belonged to and sent the data to our analytics pipeline. That let me make decisions based on real user behavior.

Here is a simple way to run an A/B test with a feature flag:

import random
import hashlib

def assign_experiment(user_id, experiment_name):
    hash_input = f"{experiment_name}:{user_id}"
    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
    bucket = hash_value % 100
    if bucket < 50:
        return "control"
    return "treatment"

bucket = assign_experiment(user.id, "new_checkout_experiment")
if bucket == "treatment":
    # show new checkout
else:
    # show old checkout

The hash ensures the same user always gets the same bucket, so their experience is consistent.

I want to share one rule I learned the hard way: never use toggles for configuration that changes rarely, like API keys or database URLs. Those belong in environment variables or a config server. Toggles are for features, not for settings.

Another anti-pattern is toggles that span multiple services. If you have a frontend toggle and a backend toggle that must be in sync, you create a coordination problem. I prefer to put the toggle in one place and have the other service ask for the decision. Or use a shared feature flag service that both services query.

When I started using feature toggles, I thought they would solve all my deployment anxiety. They did not. But they made it manageable. I could release a half-baked feature to a small audience without fear. If something went wrong, I turned it off in seconds instead of rolling back a deployment.

The most important lesson is to keep the number of active toggles low. A healthy project has fewer than ten live toggles at any time. More than that and you lose track. I review the list of toggles every sprint and archive any that are no longer needed.

I also added a monitoring alert that triggers if a toggle stays at 100% for more than two weeks. That reminds someone to remove it. Automation helps enforce discipline.

Feature toggles are a pattern, not a silver bullet. They let you separate deployment from release. You can deploy code that is not yet finished and slowly turn it on. You can experiment with confidence. You can kill a feature instantly if it misbehaves.

I still remember the day I broke production because I merged a half-finished feature. Now I would never do that without a toggle. It is the single most effective change I made to my development workflow. And it does not require any fancy tools—just a conditional statement and the discipline to clean up afterward.

Keywords: feature flags, feature toggles, feature flag management, feature toggle implementation, trunk-based development, continuous deployment, A/B testing with feature flags, gradual rollout strategy, canary deployment, feature flag best practices, runtime feature control, feature toggle patterns, software deployment strategies, feature flag tutorial, Python feature flags, Java feature toggles, JavaScript feature flags, Go middleware feature flags, feature flag database storage, feature toggle caching, feature flag cleanup, dead code removal, feature flag testing, pytest feature flags, feature flag logging, feature flag monitoring, feature toggle antipatterns, progressive delivery, release management, feature flag dashboard, user segmentation feature flags, percentage-based rollout, feature toggle lifecycle, deployment without downtime, feature branch alternative, CI/CD feature flags, feature flag service, feature flag implementation guide, feature toggle clean architecture, production deployment safety, feature flag A/B testing, software release strategy, conditional feature deployment, feature toggle removal checklist, feature flag performance, feature experiment tracking, multi-service feature flags, feature toggle complexity, feature flag for product managers, safe feature deployment



Similar Posts
Blog Image
Unit Testing Best Practices: Principles and Patterns for Writing Effective Test Code

Master unit testing principles and patterns that ensure code quality, reduce bugs, and boost deployment confidence. Learn isolation, TDD, mocking strategies, and best practices for maintainable tests.

Blog Image
**The Complete Guide to Systematic Bug Fixing: From Silent Failures to Expert-Level Debugging Strategies**

Master debugging with proven strategies and systematic approaches to solve any coding problem. Learn reproduction techniques, isolation methods, and professional debugging workflows that transform frustrating bugs into learning opportunities.

Blog Image
Is JavaScript the Secret Ingredient Behind Every Interactive Website?

JavaScript: The Dynamic Pulse That Energizes the Digital World

Blog Image
Can VHDL Unlock the Secrets of Digital Circuit Wizardry?

Decoding the Power of VHDL in Digital Circuit Design and Simulation

Blog Image
**How to Design APIs That Developers Love: Best Practices for User-Friendly Development**

Learn how to design intuitive APIs that developers love. Discover best practices for REST design, error handling, versioning, and security. Build better developer experiences.

Blog Image
8 Essential Techniques for Writing Highly Testable Code: A Developer's Guide

Discover techniques for writing testable code. Learn how to improve software quality, ease maintenance, and enhance future development. Explore dependency injection, interfaces, and more. Boost your coding skills today!