Skip to content
webkida wiki
Go back

[Feature Flag] Separating deploy and release

Separates putting code in production from showing a feature to users, and where the application reads the flag.

Putting code into production and showing that feature to users can happen at different times. A feature flag is the switch you place between them.

Keeping a feature hidden after it lands on main is covered in How Git Flow and GitHub Flow differ. Here, the question is where the flag lives, and where a request reads it.

Terms

Feature Flag

An on/off switch for one feature. The code can already be in production. If the flag is off, that feature does not reach users.

Deploy

That code is running in production. This is separate from the flag.

Release

Making the feature available to users. Turning the flag on is the release of that feature.

What a feature flag is

A feature flag is how you separate deploy from release.

You can merge a new feature to main and ship it to production with the flag still off. Users do not see it. When you decide to show it, you turn the flag on without cutting another branch.

sequenceDiagram
    participant Dev as Developer
    participant Prod as Production
    participant User as User

    Dev->>Prod: ① Merge to main and deploy
    Note over Prod: ② The code is in production<br/>the feature flag is off
    Prod-->>User: ③ Users do not see it
    Dev->>Prod: ④ Turn the feature flag on
    Prod-->>User: ⑤ Users can use it

Figure: After the deploy, the flag can stay off. Turning it on is the release of that feature.

Deploy  = put the code in production
Release = make the feature usable

GitHub Flow and trunk-based development merge small units to main instead of leaving a working branch open. A feature that is not ready to show stays behind a flag.

A flag is not required. A change that finishes quickly can be completed on the working branch and then merged to main. If the pipeline deploys on that merge, the merge deploys and releases at the same time.

How this differs from an environment variable

Feature flag is the name of the mechanism. An environment variable is one place to store the value.

if (process.env.NEW_PAYMENT_ENABLED === "true") {
  showNewPayment();
}

With NEW_PAYMENT_ENABLED=false, this branch works as a flag. What splits here is where the source value is managed, and how the application reads it.

The source value lives in an environment variable, a database, a config store, or a dedicated feature-flag service. An SDK or a sidecar is not that store. They receive state from the service and hold it locally.

sequenceDiagram
    participant App as Application
    participant Env as Environment variable
    participant DB as Database / config store
    participant Svc as Dedicated service
    participant SDK as SDK
    participant Agent as Agent

    rect rgba(120, 140, 180, 0.1)
        Note over App,Svc: Read the source directly
        App->>Env: ① Read the environment variable
        App->>DB: ② Read the database
        App->>Svc: ③ Read the dedicated service
    end

    rect rgba(120, 160, 130, 0.1)
        Note over App,Agent: Hold what they received
        Svc-->>SDK: ④ The SDK holds the state
        App->>SDK: ⑤ The app reads the SDK
        Svc-->>Agent: ⑥ The agent holds the state
        App->>Agent: ⑦ The app reads localhost
    end

Figure: The left side is where the source value lives. The right side is how the app reads it. An SDK and a sidecar hold state they received from the service.

If you stay with an environment variable, a running process does not pick up a later write. Changing it means a restart, or on ECS a new task deployment. Showing the feature to employees first, or to 5 percent of traffic, is hard with an environment variable alone.

When you want that, you add a flag service. The next question follows.

Do you ask on every request?

“Does every user request call the flag service?”

That is the first shape that comes to mind.

sequenceDiagram
    actor U as User
    participant App as Application
    participant FF as Feature-flag service

    U->>App: ① Request
    App->>FF: ② Ask for the flag
    FF-->>App: ③ Return the flag
    App-->>U: ④ Handle the request with that value

Figure: Each request asks remote. The implementation is simple, and the network sits in the middle of the response.

The implementation is simple. The worry is whether it gets slow.

Every response waits on the network. If the flag service is slow, the screen is slow. If that service stops, requests that read the flag stop with it. Calls grow with the number of requests.

Calling remote on every request is not the only shape. Common placements split according to how they take that worry off the request path.

Call remote every time

This is the shape the worry comes from. If flag reads are few and that latency is acceptable, this is enough. If it is not, move the fetch off the request.

Store it in an environment variable

This is the far end: the application does not call the service. The task receives a value such as NEW_PAYMENT=true, and the application reads process.env. No network sits in the read, so the lookup is fast.

A process that is already running does not change its value. Going from false to true means restarting the task or deploying it again. This does not fit a flag you flip often. An on/off that changes once per release can be enough.

Keep a copy in the SDK

If the request does not call remote, the value has to sit somewhere. In the SDK approach, the SDK lives inside the application process and keeps the value in memory. The user request reads that memory.

sequenceDiagram
    actor U as User
    participant App as Application
    participant SDK as SDK
    participant Mem as In-memory cache

    U->>App: ① Request
    App->>SDK: ② Ask for the flag
    SDK->>Mem: ③ Read memory only
    Mem-->>SDK: ④ true / false
    SDK-->>App: ⑤ Return the result

Figure: The request reads memory only. This diagram has no arrow to remote.

The exchange with remote stays off the request path. When the cache refreshes is a separate moment. Products use one of the following.

Polling fetches the flags at startup and keeps them in memory. On an interval, the SDK asks remote and updates the cache. That query does not make the user wait.

Streaming is the service notifying the SDK when a flag changes. The SDK updates memory from that notice. The request does not wait for the notice.

Lazy load fetches from remote at read time when the cache is stale. Only then does the network come back into the middle of the request.

Keep the copy in a sidecar

Once the cache exists, a request does not look at remote. The difference is that the copy and the refresh leave the application process. On ECS, the agent sits in the same task. On AWS, AWS AppConfig Agent is this shape.

The first fetch is the exception. AppConfig Agent builds the cache on that first retrieval. That response can be slower than later requests that already have a cache.

sequenceDiagram
    participant App as Application
    participant Agent as Sidecar / agent

    App->>Agent: ① Ask for the flag on localhost
    Agent->>Agent: ② Read the agent cache
    Agent-->>App: ③ true / false

Figure: The application reads the agent on localhost. The agent refreshes from remote outside this diagram.

In this setup, the agent does the fetch and the application reads localhost. The agent takes the latest value into its cache, and the application reads that cache. It is not a rule that the application cannot reach the AWS APIs.

SDK or sidecar

Once a cache exists, both keep remote off the request path, which is how they answer the latency worry. What remains is who refreshes that cache.

With an SDK, the application process refreshes it. With a sidecar, a separate process does. The application only reads localhost.

sequenceDiagram
    participant App as Application
    participant Mem as Memory in the same process
    participant Agent as Cache in another process

    rect rgba(120, 140, 180, 0.1)
        Note over App,Mem: SDK
        App->>Mem: ① Read memory in the same process
    end

    rect rgba(120, 160, 130, 0.1)
        Note over App,Agent: Sidecar
        App->>Agent: ② Read the cache in another process
    end

Figure: Both reads stay local. The refresh belongs to the same process, or to another one.

sequenceDiagram
    actor U as You
    participant Q as Placement

    U->>Q: ① Is remote latency on the<br/>request path acceptable?
    alt Acceptable
        Q-->>U: Call remote every time
    else Not acceptable
        U->>Q: ② Change it after shipping,<br/>without a redeploy?
        alt Rarely
            Q-->>U: Environment variable
        else You want to change it
            U->>Q: ③ Does the app process own<br/>the cache refresh?
            alt It does
                Q-->>U: SDK and memory
            else Separate it
                Q-->>U: Sidecar / agent
            end
        end
    end

Figure: First, whether the latency is acceptable. If it is not, the placement follows how often you change the flag, and who owns the refresh.

If the latency is acceptable, keep calling remote on every request. If it is not, and you rarely change the value, use an environment variable. If you still want to change the percentage or the audience after shipping, use an SDK or a sidecar. Use an SDK when the application can own the refresh. Use a sidecar when that work should leave the process.

Takeaways

The center of a feature flag is separating deploy from release.

Once a flag service is in place, the first worry is whether asking on every request makes the response slow. Asking every time is simple, and the network sits in the middle of the response. An environment variable, SDK memory, and a sidecar cache are the placements that take that worry off the path.

The decision includes where the value is stored, whether the request looks at remote, and who refreshes the cache.


Share this post:

Next Post
[Git] How Git Flow and GitHub Flow differ