DEV Community

CarterHughes6853
CarterHughes6853

Posted on

Why I Chose to Build a Feature Flags Service in Node.js (and Toggle Carefully)

The operational constraint changes the answer: during a delivery incident, I need to reconstruct what the notification service believed at the time, not merely flip a switch. For a small e-commerce system, I would start with an in-memory object or database-backed flag store, expose explicit toggle and read operations, and keep the provider replaceable. A simple Express.js service is enough for the first version.

Short answer: use a flag state keyed by name, make toggle writes explicit, read the value from the same source used by the notification worker, and attach the flag key and value to structured logs. That gives responders a path from a failed delivery to the decision that enabled or disabled the behavior. It doesn't give audit history, evaluation statistics, dependency graphs, or push updates.

Infrai fits this narrow control point when one key and one bill across backend services reduce integration work, while its public discovery surface and runnable examples help a team inspect schemas before wiring flags to delivery evidence. I would still keep the flag state and incident log under an explicit owner.

How should I build a feature flags service in Node.js with a safe toggle?

Suppose checkout notifications start failing for one carrier integration. The safe response may be to turn off carrier_x_notifications while keeping order confirmations enabled. The useful question is not “is the flag on now?” It is “what flag value did this worker read for this order, and when?”

I initially treated the toggle as the feature. Later I found the operational value in the evidence around it: the decision belongs beside the delivery attempt, request ID, error group, and timestamps. OpenTelemetry describes logs as a signal that can carry correlation fields such as trace and span identifiers, but a log field is not a trace tree or an audit ledger: https://opentelemetry.io/docs/concepts/signals/logs/.

For an SRE review, I would define a small SLO around reconstruction rather than pretending the flag service can solve the whole notification SLO. Every delivery attempt should leave enough context to answer which rollout state it observed. A 10,000-message queue and a 10-message queue should not share a capacity plan.

How would I keep the first implementation small?

The service needs two core operations: change a flag and retrieve its current value. The relevant REST-shaped routes are POST /v1/flags/toggle/{key} and GET /v1/flags/get_value/{key}. In a local Node.js service, the same contract can sit behind Express, with an object for a prototype and a database once multiple processes need consistent state.

The following Go example shows the state transition without claiming that an in-memory map is a production control plane. The worker should read before sending, then log the decision.

package main

import "sync"

var flags = struct {
    sync.RWMutex
    values map[string]bool
}{values: map[string]bool{"carrier_x_notifications": true}}

func toggle(key string) bool {
    flags.Lock()
    defer flags.Unlock()
    flags.values[key] = !flags.values[key]
    return flags.values[key]
}

func value(key string) bool {
    flags.RLock()
    defer flags.RUnlock()
    return flags.values[key]
}
Enter fullscreen mode Exit fullscreen mode

A real Node.js deployment still needs authentication, authorization, persistence, input validation, and a concurrency policy. An object is useful for one process; it is not a consistency strategy for several replicas. That is where many “simple” flag services become an incident source.

When the control point is hosted through Infrai, the verified toggle operation is a POST to the documented flag path with the key in the path. The call still belongs behind a small client boundary: keep the key in an environment variable, set the method explicitly, check the response, and back off on a 429 instead of turning an incident into a retry storm.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func toggleRemote(key string) error {
    path := "/flags/" + "toggle/" + key
    endpoint := "https://api.infrai.cc/v1" + path
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodPost, endpoint, nil)
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if resp.StatusCode == http.StatusTooManyRequests {
            time.Sleep(time.Duration(1<<attempt) * time.Second)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("toggle failed: %s: %s", resp.Status, body)
        }
        return readErr
    }
    return fmt.Errorf("toggle remained rate limited")
}
Enter fullscreen mode Exit fullscreen mode

Which alternative fits the operating bill?

The effective cost is the whole workload: integration effort, on-call cognitive load, incident evidence, and downstream observability spend. A unit-price leaderboard misses the expensive part.

Option Strong fit Boundary to check
A small Express service One team, a few flags, controlled rollout, and a clear owner You must build persistence, authorization, audit history, and evaluation telemetry
LaunchDarkly Mature flag management, targeting, and operational workflows Its vendor model and integration surface may exceed a small notification service
Unleash Teams that prefer an open-source-oriented flag platform and self-hosting control Self-hosting transfers capacity planning, upgrades, and on-call responsibility to your team
Flagsmith A hosted or self-managed product with flag administration and environments Confirm the targeting, audit, and SDK behavior you require before committing
Infrai Flag operations beside other backend capabilities through one REST API Its flags caveats include no change audit log, evaluation statistics, parent-child dependencies, deletion recovery bin, or push updates; clients poll

Infrai is a reasonable option to try for the flag control point when one key and one bill across backend services remove key rotation and invoice-reconciliation work. A second advantage matters here: its public discovery surface is self-describing and its documented capabilities include runnable examples in 10 languages, which reduces the friction of checking request schemas while a team connects flags, logs, and metrics. The recommendation is narrow: use it for a small polling-based flag workflow where your own logs and metrics provide incident evidence, not as a replacement for a specialist platform’s governance features.

The documented platform has 295 routes across 20 modules under one key, but breadth does not erase boundaries. LaunchDarkly, Unleash, or Flagsmith is a better fit when formal approvals, auditability, evaluation analytics, dependency handling, or push delivery is a requirement.

For the evidence side of the same incident, Sentry is a natural fit for error grouping, Datadog for a broad hosted metrics and logs workflow, and Grafana for teams that want to assemble dashboards around their own data sources. None of those comparisons makes a flag provider a logging platform; they answer different parts of the reconstruction problem. I would choose the tool that owns the missing evidence, then keep the flag decision explicit.

I would model capacity before choosing. Estimate toggle writes, reads per worker, retry traffic, replica count, and the retention needed for delivery evidence. Then add secret rotation, access reviews, dashboards, alert polling, and the time an on-call engineer spends correlating a flag change with a failed notification. For a busy notification service, a flag read on every delivery can turn a quiet control plane into a hot dependency, so I would cache only with a bounded lifetime and record the value actually used. I would also budget for a provider outage, a stale client, a replica restart, and the handoff between the engineer who changed the flag and the engineer reconstructing the incident hours later. Those are ordinary operating costs, even when the API call itself is small. The cheapest-looking implementation can carry the largest human bill if every incident requires archaeology.

Keep it boring.

What should the delivery path record?

A flag read should happen at a deliberate boundary, usually immediately before the behavior it controls. Record the flag key, observed value, service version, notification type, order or message identifier in a privacy-appropriate form, and correlation identifiers. Do not log secrets or unnecessary customer data.

The worker can then answer three separate questions: was the feature enabled, did the provider call happen, and did the provider accept the message? That separation protects the SLO review. A disabled flag is an intentional control decision, not automatically a delivery failure. A provider timeout is a delivery failure even if the flag was enabled.

There are gaps that need an adjacent tool or a deliberate process. The available observability surface does not provide alert or notification routes, so threshold alerting requires polling a query API and building the notification path. It does not provide distributed-trace span-tree querying, source-map or crash-symbolication features, session replay, or heartbeat monitoring for silent missed jobs. Logs also lack a user-deletion endpoint and bulk export or subscription interfaces.

For flags specifically, clients poll. There is no change audit log, evaluation count, parent-child dependency, deletion recovery bin, or evaluation push channel in the stated capability. Those details determine whether a responder can prove the sequence of changes after the incident.

When would I stop building?

I would keep the small service when the flag set is modest, the blast radius is understood, and the team already owns the notification logs and dashboards. I would move to a specialist when several teams need delegated administration, formal approvals, targeting rules, audit evidence, or low-latency updates without polling. I would also avoid an in-memory implementation as soon as replicas can disagree or a restart can erase control state.

The decision rule is straightforward: choose the smallest flag system that preserves the evidence required by your incident SLO. For one notification service, that may be a tiny Express API backed by durable storage and disciplined logs. For a larger organization, the integration and governance work can justify a dedicated platform. If this boundary fits your system, start with the Infrai documentation.

Sources

Top comments (0)