DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

A Practical Git Workflow for Feature Flags and Experimental Branches

A Practical Git Workflow for Feature Flags and Experimental Branches

A Practical Git Workflow for Feature Flags and Experimental Branches

Creating resilient software often means testing ideas safely without destabilizing the mainline. A well-thought-out Git workflow that embraces feature flags and short-lived experimental branches can help you iterate faster, reduce risk, and keep production stable. This guide outlines a concrete, step-by-step workflow with practical tooling, branching strategies, and battle-tested patterns you can adopt today.

Why feature flags and experimental branches?

  • Feature flags let you deploy code for features you’re not exposing to users yet. You can enable/disable at runtime, reduce risk, and gather incremental feedback.
  • Experimental branches let you isolate exploration work from the main branch, keeping ongoing development clean while you validate ideas.
  • Together, they enable safer experimentation, faster rollbacks, and clearer ownership.

    Overview of the workflow

  • Main branches

    • main: production-ready code, always deployable
    • develop (optional): integration for the next release; used in larger teams
  • Feature branches

    • feature/: for user-facing features behind a flag or for experiments
  • Experimental branches

    • experiment/: for exploratory work that may not be production-ready
  • Tags and releases

    • Use semantic versioning tags for stable releases
  • Flags

    • Implement feature flags via a flag management library or a config service ### Setting up the repository

Assume you have a typical repo with a Python, Node, or other stack. The core concepts apply across languages; adapt commands to your shell.

  • Ensure a clean starting point
    • git checkout main
    • git pull rebase
    • If you use develop, create it: git checkout -b develop; git push -u origin develop
  • Enable protected branches if your repo host supports it (e.g., GitHub, GitLab)

    • Require PR reviews
    • Enable status checks
    • Enforce linear history (squash or rebase) ### Branching strategy
  • Feature branches

    • Start from main or develop
    • Naming: feature/-
    • Lifecycle: short-lived; merge when the feature is behind a flag or completed
  • Experimental branches

    • Start from main or develop
    • Naming: experiment/
    • Lifecycle: long enough to validate ideas; remove or merge when decision made

Example:

  • git checkout main
  • git pull
  • git checkout -b feature/search-improvements
  • ...work...
  • git commit -m "Implement incremental search improvements behind feature flag search_v2"
  • git push origin feature/search-improvements ### Implementing feature flags

Choose a flag strategy that fits your stack.

  • Flags can be:
    • Compile-time flags (less flexible; good for large, stable toggles)
    • Runtime flags (dynamic; ideal for experiments)
    • Remote-config flags (centralized control; ideal for production)
  • Recommended approach:
    • Use runtime flags with a central configuration service or library
    • Store the flag state in a simple, observable place (e.g., a feature flag service or a config file in a safe namespace)

Code example (pseudo-implementation):

  • Python

    • Use a simple in-app flag system that reads from a config or remote service
    • Example:
    • config.py flags = {"new_search": False}
    • search.py from config import flags if flags["new_search"]: run_new_search() else: run_old_search()
  • Node.js

    • Use a library like LaunchDarkly or an open-source alternative
    • Example: const ld = require('launchdarkly-node-server-sdk'); const client = ld.init('sdk-key'); async function shouldEnableNewSearch(user) { const flag = await client.variation('new_search', user, false); return flag; }

Operational tips:

  • Treat flags as code paths: their presence should be tested and have defined behavior.
  • Add tests for both enabled and disabled states.
  • Document flags in a central registry: description, owner, date created, metrics to watch. ### Example workflow: from idea to production with flags

1) Create an experiment or feature branch

  • git checkout main
  • git pull
  • git checkout -b feature/realtime-notifs

2) Implement behind-flag code

  • Ensure the default state remains disabled
  • Add unit tests for both flag states

3) Add flag metadata

  • Create a FLAGS.md or a dedicated config section
  • Include purpose, owner, expected impact, and rollback plan

4) PR and review

  • Open PR with clear description:
    • What the feature is
    • Why it’s behind a flag
    • How to enable/disable
    • Test plan for both states

5) Deploy with flag on

  • Enable the flag for internal or alpha users
  • Monitor metrics and logs

6) Decide and act

  • If desirable, remove flag and merge fully
  • If not, keep flag for staged rollout or rollback

    Example merge checklist

  • [ ] Feature behind flag is implemented and tests pass

  • [ ] Flag state is documented and accessible

  • [ ] CI checks pass (unit, integration, lint)

  • [ ] Security and performance review completed

  • [ ] Rollback plan documented

  • [ ] PR description includes acceptance criteria and metrics

    Managing experimental branches

  • Purpose and scope

    • Clear goal: what question are you trying to answer?
    • Timeboxed: set a review date or expiration
  • Work patterns

    • Regular commits with descriptive messages
    • Frequent rebases or merges from main to stay up-to-date
  • Decision points

    • Validated: merge into main with a feature flag or remove the branch
    • Rejected: delete the branch after a decision

Example:

  • git checkout main
  • git pull
  • git checkout experiment/redesign-dashboard
  • ...work...
  • git push origin experiment/redesign-dashboard
  • Review meeting to decide on next steps

    Testing strategies for feature-flagged code

  • Unit tests for both flag states

  • Snapshot tests where UI changes with flags

  • End-to-end tests covered for both enabled and disabled states

  • Canary and synthetic monitoring after enabling a flag in production

  • Rollback tests to ensure disabling a flag returns system to expected behavior

Illustrative test split:

  • tests/test_search.py
    • test_old_search_path()
    • test_new_search_path_enabled()
  • tests/test_flags.py

    • test_flag_default()
    • test_flag_toggle() ### Production considerations
  • Observability

    • Instrument flag usage: how often is the new path hit?
    • Guardrails: set thresholds for error rates, latency, and saturation
  • Rollbacks

    • Feature flags enable quick toggling without redeploys
    • Keep the old behavior fully retained as a code path
  • Data integrity

    • If the new feature mutates data, ensure migrations are reversible
    • Use backward-compatible database changes when possible ### Tools and tips
  • Version control

    • Use descriptive branch names and a concise PR title
    • Enforce PR templates with fields for goal, risk, testing, rollback
  • Feature flag management

    • Local development: mock flags to test both paths
    • Production: centralize flag definitions for consistent behavior
  • Collaboration

    • Assign flag owners and set SLAs for reviewing experimental work
    • Use changelog entries to reflect flag introductions and removals ### A concrete example: a UI redesign behind a flag
  • Goal: test a new dashboard layout with a subset of users

  • Branch: feature/dashboard-redesign

  • Implementation steps:

    • Refactor UI components to support two layouts
    • Add flag checks around the layout switch
    • Update tests to cover both layouts
    • Document in FLAGS.md: dashboard_redesign, owner, rollout plan
  • Rollout:

    • Enable flag for internal users first
    • Monitor metrics like time-to-task-completion, click-through rates
    • If metrics improve, broaden rollout; if not, revert flag ### Common pitfalls and how to avoid them
  • Pitfall: Flags proliferate and become technical debt

    • Solution: retire flags promptly after decisions; create a flag registry; automatically log usage to inform cleanup
  • Pitfall: Hidden flag states in production

    • Solution: ensure all paths are visible in diagnostics; have a separate flag toggle interface for operators
  • Pitfall: Inconsistent flag behavior across services

    • Solution: centralize flag definitions or enforce contract tests across services ### Sample starter repository changes
  • Add a feature flag module

    • src/flags.py (or flags.go, flags.ts)
    • Central registry with default values and description
  • Refactor a component

    • Wrap new behavior in an if flags.get("new_feature"):
    • Maintain old behavior behind the flag
  • Documentation

    • FLAGS.md detailing each flag, owner, and rollout plan

Code sketch (Python-like pseudocode):

  • flags.py
    class FlagService:
    def init(self, storage):
    self.storage = storage
    def is_enabled(self, key, default=False):
    return self.storage.get(key, default)

  • usage
    from flags import FlagService
    flags = FlagService(storage={"new_dashboard": False})
    if flags.is_enabled("new_dashboard"):
    render_new_dashboard()
    else:
    render_old_dashboard()

    Conclusion

A workflow that combines feature flags with disciplined experimental branches empowers you to iterate quickly while maintaining stability. By isolating changes, documenting decisions, and embedding robust testing and observability, you can experiment boldly and roll back gracefully when needed. Start small: pick one feature flag, one experimental branch, and establish a clear rollout and cleanup plan. Once the pattern proves valuable, extend it across your team.

Would you like a ready-to-adapt GitHub Actions workflow and a minimal code example in your preferred stack (Python, Node.js, or Go) to kick this off? If so, tell me your tech stack and any flag management preferences.

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)