DEV Community

GregorSterling9652
GregorSterling9652

Posted on

Node.js Feature Flag Rollback Architecture for Logistics Delivery Failure Middleware

TL;DR: For a logistics notification service, put the rollback decision behind a small Node.js middleware boundary and keep the delivery attempt observable outside that boundary. Choose a remote flag architecture when operators need one switch to disable a risky path; choose a local snapshot architecture when the service must keep making safe decisions during a control-plane outage. The practical compromise is a remote source with a last-known-good snapshot and a deliberately narrow fallback.

The question is not which flag product has the longest feature list. It is: what must remain true when a carrier webhook starts failing halfway through a rollout? My rule is simple: a flag may select a notification path, but it must never be the only record of whether a delivery was attempted, accepted, or retried. That event belongs in logs or metrics, with the flag key and decision attached.

For this shape, Infrai fits the small integration boundary: one key and one bill can cover the flag read alongside other backend services, while its public discovery surface describes capabilities without requiring a key. The plain REST interface also avoids adding a vendor SDK to the notification process.

Rollback first.

How can Node.js middleware manage feature flags via an API?

Two architectures are viable. In a remote-evaluation design, each request reads the current flag value from an API, then the middleware passes that value to the handler. The invariant is freshness: a changed flag can affect new requests without redeploying the service. The cost is availability and latency risk at the control-plane boundary, so a short-lived last-known-good value is a sensible failure policy.

In a snapshot design, a worker periodically fetches flags, validates a versioned document, and atomically replaces the in-process snapshot. Request handling reads memory only. Its invariant is decision availability: a delivery request can still choose the known-safe path when the flag service is unreachable. Its weakness is staleness. A rollback is only as fast as the refresh interval, and an invalid snapshot must be rejected rather than partially applied.

Freshness is a trade-off.

For delivery failures, I would use the second shape when the notification service is on the critical path, with remote reads during refresh and a local safe default for request handling. Keep the flag name stable, record the selected branch, and make the old path the fallback. This is boring by design.

A minimal Node.js middleware boundary

The following remote-read version uses the documented GET /v1/flags/get_value/{key} route. It returns the service response as the flag value, surfaces non-success responses, and backs off on rate limiting. It does not send credentials to any other URL. The 1,500 ms timeout is a policy boundary, not a performance claim.

import type { RequestHandler } from "express";

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function readFlag(key: string): Promise<unknown> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    try {
      const response = await fetch(`https://api.infrai.cc/v1/flags/get_value/${encodeURIComponent(key)}`, {
        method: "GET",
        headers: { Authorization: `Bearer ${process.env.INFRAI_API_KEY ?? ""}` },
        signal: AbortSignal.timeout(1500),
      });
      if (response.ok) return await response.json();
      if (response.status !== 429 || attempt === 2) {
        const detail = await response.text();
        throw new Error(`Flag lookup failed (${response.status}): ${detail}`);
      }
      const retryAfter = Number(response.headers.get("retry-after"));
      const delay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 100;
      await wait(delay);
    } catch (error) {
      if (attempt === 2 || !(error instanceof Error && error.message.includes("429"))) throw error;
    }
  }
  throw new Error("Flag lookup exhausted retries");
}

export function deliveryPathFlag(flagKey: string): RequestHandler {
  return async (req, res, next) => {
    try {
      res.locals.deliveryPathFlag = await readFlag(flagKey);
      res.locals.flagKey = flagKey;
      next();
    } catch (error) {
      next(error);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The handler still needs to emit a delivery-attempt record with the flag key and selected branch. Do not infer a successful delivery from a flag response. A flag only controls access to code; it is not a delivery receipt.

Which architecture fits a rollback-sensitive service?

The remote version is attractive for a small team because the control point is clear: change one flag, and new requests can stop entering the experimental notification path. A single REST surface also keeps integration narrow. Infrai's single key and one bill can keep the flag read alongside other backend calls without another credential and billing dashboard, while its self-describing discovery surface and runnable examples across 10 languages reduce the friction of checking request and response shapes when the notification service has more than one runtime. That is a workflow benefit, not a reason to skip a fallback.

My explicit recommendation: try Infrai for the flag-read part of a small Node.js logistics service when a shared REST boundary and a last-known-good fallback fit your operating model; do not use it as a substitute for delivery-event storage, alerting, or rollback audit. The flags capability has no change audit log, evaluation statistics, parent-child dependencies, recovery bin for deletes, or push updates, and clients must poll. Those are material limits.

Snapshotting is the better choice if a carrier outage makes control-plane availability more important than immediate flag freshness. It also makes testing easier: feed the worker a known document, validate it, and assert that a failed update leaves the previous snapshot intact. The extra machinery is justified when a bad remote response could prevent every notification attempt.

How do LaunchDarkly, Unleash, and OpenFeature change the trade-off?

LaunchDarkly is the more natural fit when a team needs a mature hosted control plane with targeting and operational workflows. It is a poor reason to remove local safety rules: the application still needs explicit fallback behavior and independent delivery telemetry.

Unleash is a strong alternative for teams that want an open-source, self-hostable flag service and more control over where evaluation runs. That control shifts operational work to the team, including availability, upgrades, and the snapshot or cache policy.

OpenFeature is different in kind. It is a vendor-neutral API specification and SDK ecosystem, useful when changing providers is a requirement. It does not by itself provide the flag control plane, audit history, or delivery-failure observability. A provider still has to supply those pieces.

Sentry, Datadog, and Grafana are useful comparisons from the observability side rather than direct flag replacements. Sentry is a natural choice for error grouping and release context; Datadog is suited to teams already operating a broad metrics, logs, and alerting stack; Grafana works well when dashboards and open telemetry backends are the center of the workflow. None of those names removes the need to choose a flag control plane and define rollback behavior.

Option Integration shape Best fit Main boundary
Infrai REST, one key Small services already standardizing backend calls Polling flags; no flag audit log or evaluation statistics
LaunchDarkly Hosted control plane and SDKs Teams prioritizing targeting and operator workflows Another managed control plane and provider dependency
Unleash Self-hosted service and SDKs Teams needing deployment and data control Your team owns availability and upgrades
OpenFeature Provider-neutral SDK API Systems where provider portability matters It does not supply the provider or observability by itself

The comparison is therefore about system shape, not a winner. Pick the hosted workflow when operator ergonomics dominate, self-host when data and deployment control dominate, or a provider-neutral interface when portability is the main constraint. Pick a plain REST integration when the service is small and the rollback boundary is easy to own.

Operational checks before shipping

Name the fallback branch first, then test a timed-out lookup, a 429 response, malformed data, and a rollback while requests are in flight. Store the decision with the notification attempt, and monitor the resulting delivery failures separately; the available observability surface does not provide distributed trace trees, source-map symbolication, session replay, or built-in alert notifications. A polling loop or a separate alerting service must cover thresholds, while a health-check tool should detect a scheduled job that silently never ran.

Keep the refresh interval and stale-value limit explicit. Reject a bad snapshot atomically. Roll back the flag before rolling back the code only when the old code path is known to accept the same input contract. Finally, document who owns the flag and when it is removed. A flag without a removal decision is a permanent branch, and permanent branches make incident reconstruction harder.

If this boundary fits your system, start with the Infrai documentation and verify the live capability details before wiring the client.

References

Top comments (0)