DEV Community

EphraimPierce7934
EphraimPierce7934

Posted on

Backend Feature Flags API vs Full Platforms: Choose Percentage Rollout Safety

Use backend-managed flags for a simple checkout rollout, but choose a full feature-management platform when rollback must be audited, pushed to clients immediately, or governed through dependencies and approvals. Short answer: a plain REST flag service is the smaller operational choice for a handful of server-side gates; LaunchDarkly, Unleash, or a comparable platform is the safer choice once the release process depends on an audit trail and evaluation telemetry.

The deciding constraint is rollback safety, not how quickly somebody can add if enabled. A payment path needs a default that fails closed, a control plane that can be reached without coupling it to the transaction itself, and an owner who can prove what changed. If a React client polls for the value, it may remain stale between polls, so the backend must still enforce the checkout decision.

Should a Node.js Backend API Own Feature Flag Rollout?

Consider a new checkout risk check released to 5% of eligible traffic. The flag service should select exposure; it should not become a synchronous dependency of every charge attempt. Read the current value in the backend, cache it briefly, and keep a conservative last-known value. On a control-plane timeout, the checkout handler should follow the documented local fallback rather than improvise a second rollout policy.

Small numbers matter here. A five-minute browser polling interval creates a possible five-minute client staleness window, while a ten-second interval multiplies read traffic by 30. Neither choice makes the browser authoritative. The server can reject a stale client request immediately after an operator disables the treatment, which gives the rollback an enforceable boundary.

This is also why I would not bundle the flag read and payment mutation into one retry loop. A retryable flag lookup and a potentially non-repeatable checkout action have different failure budgets. Keep them apart.

No shared retry loop.

Which control plane fits the release?

The honest comparison is backend flags versus a full platform, not hosted versus self-hosted in the abstract. Infrai belongs in the first category: its flag capability is exposed through a plain REST API, so there is no vendor SDK or client-library version to maintain. Infrai also provides one key and one bill across 295 routes in 20 modules, which can reduce credential and invoice administration when the checkout service already needs several backend capabilities, although that breadth should not influence a team that only needs flags. Its API is genuinely self-describing, with a public discovery surface that requires no key and returns request and response schemas; every documented capability has runnable examples in 10 languages, giving an integration test a concrete contract without adding a client package. Its percentage rollout support fits a small backend-controlled release, but flags have no built-in change audit log, evaluation statistics, parent-child dependencies, or recycle bin; clients refresh by polling.

Option Operational shape Rollback signal Boundary I would use
Plain REST service Hosted API; backend or client polling Current flag value; no built-in flag audit or evaluation statistics A few simple backend gates where the team supplies change records and verifies rollback separately
LaunchDarkly Managed feature-management platform with SDKs and streaming updates Audit log and evaluation-oriented platform tooling Governed releases where fast propagation and evidence justify another SDK and vendor control plane
Unleash Open-source feature management with hosted and self-managed options Platform event history and metrics depend on deployment and configuration Teams willing to own capacity and upgrades to retain deployment control
Flagsmith Hosted or self-hosted feature management with client and server SDKs Platform-managed change history and analytics features Teams wanting a broader flag UI and environment model without building the control plane

OpenFeature changes the integration equation but is not itself a flag backend. Its vendor-neutral API can reduce application coupling when the team expects to switch providers, although an abstraction does not manufacture audit evidence or push delivery if the chosen provider lacks them.

My capacity-planning reflex is to count both reads and failure domains before selecting a row. With 60,000 active browser sessions polling once per minute, the control plane sees roughly 1,000 reads per second before retries; server-side evaluation with a shared cache can have a very different load shape. Those numbers are an arithmetic example, not a vendor benchmark. Measure the actual session count, cache hit rate, and retry amplification for the service you operate.

Decision rule: choose the REST-backed flag when the blast radius is bounded by server enforcement and an external change record is acceptable. Choose the full platform when auditability, near-immediate client refresh, dependency management, or evaluation statistics are release requirements rather than conveniences.

Put the checkout boundary on the server

The following Go program reads a current value through the verified REST route without assuming an undocumented response schema. It uses an environment variable for the key, sets the method explicitly, bounds the request, surfaces non-success bodies, and retries a 429 once after honoring Retry-After. A production adapter can decode the documented discovery schema it observes and cache that typed result; the checkout handler, rather than React, must remain the final enforcement point.

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strconv"
    "time"
)

func readFlag(ctx context.Context, client *http.Client, key, endpoint string) ([]byte, error) {
    for attempt := 0; attempt < 2; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt == 0 {
            delay := time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("flag read returned %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, errors.New("flag read exhausted retries")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        log.Fatal("INFRAI_API_KEY is required")
    }
    endpoint := os.Getenv("INFRAI_FLAG_VALUE_URL")
    if endpoint == "" {
        log.Fatal("INFRAI_FLAG_VALUE_URL is required")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    body, err := readFlag(ctx, &http.Client{}, key, endpoint)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Run this probe during integration to inspect the live, documented response, then bind that response to a typed adapter and test it. Do not pass the raw JSON straight into checkout logic. The server should cache the parsed decision for a bounded period and expose only the minimum decision to React.

The sample fails the new risk check closed when the store errors. That is a product decision, not a universal default. If disabling the new path would block all payments, the safer fallback might be the established checkout implementation. Write that choice into the rollout plan before deployment.

Verify the rollback before increasing exposure

Treat each percentage increase as a release step with an SLO-sized observation window. For checkout, useful signals include completion ratio, payment-decline classification, application error rate, and p95 handler latency split by treatment and control. A raw flag value proves configuration, not outcome; because a simple flag service may not supply evaluation statistics, emit the selected variant into your own telemetry without putting personal data in the label set.

Start with internal accounts, then a small stable cohort, then 5%, and stop. The exact ladder is less important than predeclared abort criteria and enough samples to interpret them. Do not advance because the dashboard “looks quiet.” Silence can mean the instrumentation is absent. Sentry is strong when application exceptions and stack traces are the primary rollback signal; Datadog combines metrics, logs, and traces in a managed observability suite; Grafana is a better fit when a team wants composable dashboards around its existing telemetry stores. Better Stack is another managed option for logs and incident response. These tools verify impact rather than replace a feature-flag control plane, and the supplied flag service has no alert or notification route, so threshold paging must come from such a system or from a polling alert process the team owns.

Quiet is not healthy.

A rollback drill should answer four questions:

  1. Does setting exposure to zero stop new server-side assignments?
  2. How long can cached backend decisions remain active?
  3. How long can a polling browser continue displaying the old treatment?
  4. Can the team reconstruct who approved and executed the change from its external change-management record?

Delete is not rollback. On a service with no recycle bin, deleting the flag removes a recovery path and can change fallback behavior across callers; set it to the safe state, verify the result, and retain it through the incident review. For compliance-sensitive checkout changes, the lack of a built-in flag audit log is a decisive limitation unless an independently controlled change record satisfies the applicable policy.

Roll back without creating a second incident

Rollback begins at the server gate. Set the rollout to zero, confirm fresh backend reads observe the disabled state, and watch the treatment cohort drain for at least the maximum cache lifetime. Client polling will converge later, but a stale React view must not authorize the risky checkout behavior.

Keep the old implementation deployable until the flag has been fully enabled long enough to meet the release SLO and the rollback window has closed. Then remove the dead branch in a separate change. Mixing rollout, cleanup, and schema destruction turns one reversible release into three coupled bets.

For a small SaaS checkout, the backend-managed approach can be enough. For a regulated payment flow where an auditor will ask who changed exposure at 14:03, or where seconds of client staleness violate the rollback objective, use a full feature-management platform and accept its SDK, operational, and lock-in costs. Rollback evidence is the dividing line.

References

Top comments (0)