DEV Community

BramwellVance7953
BramwellVance7953

Posted on

Simple Feature Flags API vs Targeting Suites — Pick Simplicity for 4-Step Rollbacks

Short answer: choose a simple server-side feature flags API for an edtech pricing rule when percentage rollout and a fast off switch are the whole job; choose a fuller targeting suite when audit evidence, evaluation analytics, dependencies, or instant client updates are part of the rollback contract.

The page arrives first. An on-call engineer sees the checkout SLO crossing its rollback threshold while a new district pricing rule is exposed to a small percentage of traffic. The useful action is to stop exposure, confirm that the old calculation is active, and then inspect which cohort moved. A flag helps only if those actions fit inside the stated rollback budget.

No drama. Just control.

Incident minute 0: reliability starts with the cohort

Treat this as a hypothetical 4-step runbook, not an incident story: freeze the ramp, turn off district_pricing_v2, verify the previous calculation, and compare the affected cohort with the control. The flag is the actuator. It is not the evidence that tells the engineer to pull it.

The earlier signal should combine a business SLI, such as successful checkout completion, with the evaluated flag state and a stable cohort identifier. Request latency is useful context, while Core Web Vitals can expose a rendering regression in the checkout experience; none of those measurements alone proves that the pricing rule caused the change. The rollback threshold has to be written before the ramp begins, because a threshold chosen while a page is firing tends to follow anxiety rather than an error budget.

This is where capacity planning enters a feature-flag decision. A 1% cohort may produce too little traffic for a useful comparison, while an aggressive threshold may page on normal class-period bursts. I'm not sure there is one correct hold time across edtech products — enrollment volume, district size, and the acceptable delay before rollback determine it — but the team can decide in advance how much evidence is enough.

False positives have a real cost. Every unnecessary page consumes the same on-call attention needed for an actual pricing regression, and repeated false alarms teach responders to hesitate when the off switch should be boring. Suppose the page says only “checkout down” during the morning login surge: the responder must first separate normal traffic shape from the flagged cohort, then locate the relevant release, then decide whether rollback is justified. Put the flag state and cohort on the page instead, and those three investigative branches collapse into one reversible action. That is the practical difference between collecting telemetry and designing an alert.

How can a Node.js Express API implement percentage-based feature flags and user targeting?

Keep the Express request path small: poll the server-side flag state, cache the last confirmed result, and branch between the existing and new pricing functions. The flags capability supports create/update operations, enabled-state checks, value reads, and percentage rollout, so the team does not need to build percentage hashing before the first gradual release. Clients must poll because there is no realtime push mechanism.

User targeting needs a sharper definition. If it means “this stable percentage receives the new rule,” the rollout capability covers the release mechanism. If it means policy such as district-specific eligibility, exclusions, nested prerequisites, or evidence of every evaluation, a basic toggle should not be stretched into a policy engine. Keep business eligibility in the application or select a fuller platform, and make the flag decide only whether the new code path may run.

This runnable Go client reads the enabled state through the verified route. INFRAI_BASE_URL must contain the API base URL in the deployment environment; keeping the unlinked article free of a vendor URL does not change the runtime contract. The route template remains visible for review, and the client replaces only its key placeholder.

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    baseURL := strings.TrimSuffix(os.Getenv("INFRAI_BASE_URL"), "/")
    apiKey := os.Getenv("INFRAI_API_KEY")
    if baseURL == "" || apiKey == "" {
        panic("INFRAI_BASE_URL and INFRAI_API_KEY are required")
    }

    key := url.PathEscape("district_pricing_v2")
    route := strings.Replace("/v1/flags/is_enabled/{key}", "{key}", key, 1)

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, baseURL+route, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Accept", "application/json")

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                wait = time.Duration(seconds) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Errorf("flag read returned %s: %s", resp.Status, body))
        }

        fmt.Println(string(body))
        return
    }

    panic("flag read remained rate limited after 4 attempts")
}
Enter fullscreen mode Exit fullscreen mode

Polling interval is an SLO choice. A client that refreshes every 30 seconds cannot promise a 5-second configuration rollback, and shortening the interval increases query traffic. Cache only as long as the rollback objective permits, preserve the previous pricing function, and test the off path before exposing the first cohort. The API client must use Bearer authentication from an environment-held key, set an explicit method, check non-success responses, and back off on HTTP 429; a rollout write also needs an idempotency key so a retry cannot apply the change twice.

Evaluate recovery before changing the next cohort

For every pricing evaluation, emit enough structured context to separate the control path from the new path: the flag key, evaluated state, stable cohort identity, pricing-rule version, and request ID. Count successful and rejected checkouts by cohort, and measure the calculation path's latency. Logs can carry trace_id and span_id for correlation, but this observability capability does not provide a distributed-trace query or span tree, so don't design the investigation as though it does.

The alerting loop also sits outside the capability. There is no threshold-rule, phone, SMS, or webhook notification route; a team using these metrics must poll the free query API and connect the result to an alerting system it already operates. Scheduled silent failures need a Healthchecks-style monitor because synthetic checks and heartbeat monitoring are not included. Source-map decoding, crash symbolication, Electron minidump parsing, and Session Replay are outside this surface as well.

That boundary matters more than a long feature list. The first page should identify the breached SLO and cohort; the responder should not have to infer both from a generic error counter after customers report the problem.

Governance decides what the team buys after recovery

The buy-versus-build line belongs at the evidence requirement. Infrai fits the narrow case because one key and one bill cover 295 routes across 20 modules; for this pricing rollout, that broad, consistent surface is useful only after the team accepts polling and owns the surrounding alert loop.

Choice Best fit for this rollout Evidence and control trade-off Platform-team cost
Basic flags API Percentage ramp plus a direct off switch No flag change audit, evaluation analytics, parent-child dependencies, or restore after deletion Small HTTP integration; team owns polling, targeting policy, and alerts
LaunchDarkly Evaluate when governed targeting is required Verify its current workflow against the required approvals and evaluation evidence Additional vendor control plane and operating conventions
Unleash Evaluate when ownership model is a primary constraint Verify the chosen deployment and edition against the governance checklist Capacity and on-call implications depend on the selected operating model
Flagsmith Evaluate as another dedicated flag-platform candidate Verify targeting and audit requirements against current documentation Another control plane to integrate and capacity-plan
Sentry Error evidence around a failed release Complements rather than replaces the flag actuator Team must connect error evidence to the rollback runbook
Datadog Centralized operational signals and alerting Complements rather than replaces the flag actuator Broader telemetry surface requires deliberate tagging and ownership
Grafana Dashboards and alerts over team-selected data sources Complements rather than replaces the flag actuator Team owns data-source and alert-rule design
Application-owned flags Tiny, stable rules with strong internal ownership Every rollout, history, and evaluation control must be built and tested Full maintenance and on-call burden stays with the team

The catch is explicit: the simple option is not suitable when a regulated release needs to prove who changed a flag, when evaluation statistics are required for the decision, when flags depend on other flags, or when deletion must be reversible. Stick with a fuller platform after validating LaunchDarkly, Unleash, or Flagsmith against those requirements. Also avoid a polling-only client when the rollback SLO cannot tolerate stale state.

Lock-in deserves one line, not a sermon. Put the pricing decision behind an internal interface and keep provider response shapes out of domain code; then a later provider change touches the adapter rather than every Express handler.

Rollout resumes only with a written stop condition

Start with the smallest cohort that still produces decision-grade traffic, not an arbitrary fashionable number. Define the checkout SLO, the allowable error-budget burn, the observation window, and the exact off-switch condition. Only then select the initial percentage and subsequent ramp steps.

The rollout can be simple. The decision cannot.

A practical release record should capture the flag key, cohort rule, start time, planned hold, owner, rollback threshold, and previous pricing version. That record is especially important with a basic API because the flag capability has no change audit log or evaluation analytics. Do not delete the flag during the release: deletion has no trash or restore path, while disabling preserves a recoverable control.

For this edtech rule, the decision is therefore conditional but clear. Use the simple server-side flag when percentage exposure, polling, and an application-owned targeting rule fit the SLO. Move to a fuller targeting suite when governance or near-instant propagation becomes part of correctness. A cheap integration that cannot satisfy the rollback budget is still expensive at 02:00.

References

Further reading

Top comments (0)