---
title: Local Flag Overrides with the Multi-Provider Pattern
description: >-
  Use OpenFeature's Multi-Provider pattern with an in-memory provider to
  override flag variants locally for testing.
breadcrumbs: >-
  Docs > Feature Flags > Implementation Patterns > Local Flag Overrides with the
  Multi-Provider Pattern
---

> For the complete documentation index, see [llms.txt](https://docs.datadoghq.com/llms.txt).

# Local Flag Overrides with the Multi-Provider Pattern

{% callout %}
# Important note for users on the following Datadog sites: app.ddog-gov.com, us2.ddog-gov.com

{% alert level="danger" %}
This product is not supported for your selected [Datadog site](https://docs.datadoghq.com/getting_started/site.md). ({% placeholder "user-datadog-site-name" /%}).
{% /alert %}

{% /callout %}

## Overview{% #overview %}

When you test or demo a feature flag integration, you often need to force a specific variant without changing the flag configuration in Datadog. The [OpenFeature Multi-Provider](https://openfeature.dev/specification/appendix-a/#multi-provider) pattern combines an `InMemoryProvider` with your Datadog provider so local overrides take precedence while all other flags continue to resolve from Datadog.

Typical use cases include:

- QA and manual testing of specific flag variants
- Local development without editing flag configuration in the UI
- Demos and support workflows where a teammate needs a predictable variant on demand

{% alert level="warning" %}
Use local overrides only in non-production environments. Restrict or disable this pattern in production builds so end users cannot change flag behavior through local configuration.
{% /alert %}

If your application runs in the browser, the **Feature Flags** tab in the [Datadog Browser SDK developer extension](https://docs.datadoghq.com/feature_flags/browser_developer_extension.md) provides the same local override behavior through a Chrome DevTools panel, without the provider composition described on this page.

## How local flag overrides work{% #how-local-flag-overrides-work %}

OpenFeature's Multi-Provider wraps multiple providers behind a single client. By default, it uses the *First Match* strategy:

1. If the first provider returns a value for the flag, that value is used and later providers are skipped.
1. If the first provider returns `FLAG_NOT_FOUND`, evaluation continues to the next provider.
1. Datadog serves as the fallback for any flag not present in the in-memory override set.

Providers are evaluated in the order you register them. Place the `InMemoryProvider` *first* in the provider list so overrides take precedence over Datadog.

For more detail on evaluation strategies, see [First Match strategy](https://openfeature.dev/specification/appendix-a/#first-match-strategy) in the OpenFeature specification.

## Client override examples{% #client-override-examples %}

{% alert level="info" %}
The Multi-Provider override pattern is supported on [JavaScript](https://docs.datadoghq.com/feature_flags/client/javascript.md), [React](https://docs.datadoghq.com/feature_flags/client/react.md), [Angular](https://docs.datadoghq.com/feature_flags/client/angular.md), [React Native](https://docs.datadoghq.com/feature_flags/client/reactnative.md), [iOS](https://docs.datadoghq.com/feature_flags/client/ios.md), and [Android](https://docs.datadoghq.com/feature_flags/client/android.md). On iOS and Android SDKs, `MultiProvider` is available, but the OpenFeature SDKs do not ship a built-in in-memory provider. Implement a small custom `FeatureProvider` instead, as shown in the [iOS](https://docs.datadoghq.com/feature_flags/client/ios.md#testing) and [Android](https://docs.datadoghq.com/feature_flags/client/android.md#testing) testing documentation.
{% /alert %}

The following examples use browser JavaScript and show two common ways to populate overrides. Adapt the `loadOverrides` function to match how your team prefers to set overrides, or to the storage mechanism available on your platform (for example, `UserDefaults` on iOS or `SharedPreferences` on Android).

### URL query parameters{% #url-query-parameters %}

Read overrides from the page URL at startup. This approach is useful when QA or support needs to share a link that forces specific variants:

```javascript
import { InMemoryProvider, MultiProvider, OpenFeature } from '@openfeature/web-sdk';
import { DatadogProvider } from '@datadog/openfeature-browser';

const OVERRIDE_QUERY_PREFIX = 'ff.';

function buildInMemoryFlags(overrides) {
  return Object.fromEntries(
    Object.entries(overrides).map(([flagKey, value]) => [
      flagKey,
      {
        variants: { forced: value },
        defaultVariant: 'forced',
        disabled: false,
      },
    ]),
  );
}

function loadOverrides() {
  const overrides = {};
  const params = new URLSearchParams(window.location.search);

  for (const [key, value] of params.entries()) {
    if (!key.startsWith(OVERRIDE_QUERY_PREFIX)) continue;
    const flagKey = key.slice(OVERRIDE_QUERY_PREFIX.length);
    if (flagKey) overrides[flagKey] = value;
  }

  return overrides;
}

export async function initializeFeatureFlags({
  clientToken,
  applicationId,
  env,
  service,
}) {
  const datadogProvider = new DatadogProvider({
    clientToken,
    applicationId,
    env,
    service,
  });

  await OpenFeature.setProviderAndWait(
    new MultiProvider([
      { provider: new InMemoryProvider(buildInMemoryFlags(loadOverrides())) },
      { provider: datadogProvider },
    ]),
  );
}
```

A tester could append `?ff.checkout_new=true&ff.ui_theme=dark` to the page URL to force those variants.

### localStorage{% #localstorage %}

Persist overrides across page reloads by reading from `localStorage`. This approach is useful when a developer toggles flags from a local debug panel:

```javascript
import { InMemoryProvider, MultiProvider, OpenFeature } from '@openfeature/web-sdk';
import { DatadogProvider } from '@datadog/openfeature-browser';

const OVERRIDE_STORAGE_KEY = 'ff_overrides';

function buildInMemoryFlags(overrides) {
  return Object.fromEntries(
    Object.entries(overrides).map(([flagKey, value]) => [
      flagKey,
      {
        variants: { forced: value },
        defaultVariant: 'forced',
        disabled: false,
      },
    ]),
  );
}

function loadOverrides() {
  const raw = localStorage.getItem(OVERRIDE_STORAGE_KEY);
  if (!raw) return {};

  try {
    return JSON.parse(raw);
  } catch {
    return {};
  }
}

export async function initializeFeatureFlags({
  clientToken,
  applicationId,
  env,
  service,
}) {
  const datadogProvider = new DatadogProvider({
    clientToken,
    applicationId,
    env,
    service,
  });

  await OpenFeature.setProviderAndWait(
    new MultiProvider([
      { provider: new InMemoryProvider(buildInMemoryFlags(loadOverrides())) },
      { provider: datadogProvider },
    ]),
  );
}
```

Set overrides from a debug panel or the browser console, then reload the page:

```javascript
localStorage.setItem('ff_overrides', JSON.stringify({
  checkout_new: true,
  ui_theme: 'dark',
}));
```

## Server override examples{% #server-override-examples %}

On the server, populate the in-memory provider at process startup from any local source. Environment variables and static configuration maps are two common approaches.

{% alert level="info" %}
The Multi-Provider utility is available in the [Node.js](https://docs.datadoghq.com/feature_flags/server/nodejs.md), [Go](https://docs.datadoghq.com/feature_flags/server/go.md), [Java](https://docs.datadoghq.com/feature_flags/server/java.md), [PHP](https://docs.datadoghq.com/feature_flags/server/php.md), and [.NET](https://docs.datadoghq.com/feature_flags/server/dotnet.md) OpenFeature SDKs.
{% /alert %}

### Environment variables{% #environment-variables %}

Read overrides from environment variables at startup. This approach is useful for one-off local runs or CI jobs that need a specific variant:

{% tab title="Node.js" %}

```javascript
import { MultiProvider, OpenFeature, TypedInMemoryProvider } from '@openfeature/server-sdk';
import tracer from 'dd-trace';

const OVERRIDE_ENV_PREFIX = 'FF_';

function buildInMemoryFlags(overrides) {
  return Object.fromEntries(
    Object.entries(overrides).map(([flagKey, value]) => [
      flagKey,
      {
        variants: { forced: value },
        defaultVariant: 'forced',
        disabled: false,
      },
    ]),
  );
}

function loadOverrides() {
  const overrides = {};

  for (const [key, value] of Object.entries(process.env)) {
    if (!key.startsWith(OVERRIDE_ENV_PREFIX) || value === undefined) continue;
    const flagKey = key.slice(OVERRIDE_ENV_PREFIX.length);
    if (flagKey) overrides[flagKey] = value;
  }

  return overrides;
}

export async function initializeFeatureFlags() {
  tracer.init();

  const overrideProvider = new TypedInMemoryProvider(
    buildInMemoryFlags(loadOverrides()),
  );

  await OpenFeature.setProviderAndWait(
    new MultiProvider([
      { provider: overrideProvider },
      { provider: tracer.openfeature },
    ]),
  );
}
```

{% /tab %}

{% tab title="Go" %}

{% alert level="info" %}
The Go Multi-Provider package is experimental and may change in future releases.
{% /alert %}

```go
const overrideEnvPrefix = "FF_"

func loadOverrides() map[string]any {
    overrides := make(map[string]any)

    for _, entry := range os.Environ() {
        parts := strings.SplitN(entry, "=", 2)
        if len(parts) != 2 || !strings.HasPrefix(parts[0], overrideEnvPrefix) {
            continue
        }
        flagKey := strings.TrimPrefix(parts[0], overrideEnvPrefix)
        if flagKey != "" {
            overrides[flagKey] = parts[1]
        }
    }

    return overrides
}

func buildInMemoryFlags(overrides map[string]any) map[string]memprovider.InMemoryFlag {
    flags := make(map[string]memprovider.InMemoryFlag, len(overrides))

    for flagKey, value := range overrides {
        flags[flagKey] = memprovider.InMemoryFlag{
            State:          memprovider.Enabled,
            DefaultVariant: "forced",
            Variants:       map[string]any{"forced": value},
        }
    }

    return flags
}

func initializeFeatureFlags() error {
    tracer.Start()
    defer tracer.Stop()

    datadogProvider, err := ddopenfeature.NewDatadogProvider(ddopenfeature.ProviderConfig{})
    if err != nil {
        return err
    }
    defer datadogProvider.Shutdown()

    overrideProvider := memprovider.NewInMemoryProvider(buildInMemoryFlags(loadOverrides()))

    multiProvider, err := multi.NewProvider(
        multi.StrategyFirstMatch,
        multi.WithProvider("overrides", overrideProvider),
        multi.WithProvider("datadog", datadogProvider),
    )
    if err != nil {
        return err
    }

    return openfeature.SetProviderAndWait(multiProvider)
}
```

{% /tab %}

{% tab title="Java" %}

{% alert level="info" %}
The Java Multi-Provider API is experimental and may change in future releases.
{% /alert %}

```java
private static final String OVERRIDE_ENV_PREFIX = "FF_";

private static Map<String, Object> loadOverrides() {
    Map<String, Object> overrides = new HashMap<>();

    for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
        String key = entry.getKey();
        if (!key.startsWith(OVERRIDE_ENV_PREFIX)) {
            continue;
        }
        String flagKey = key.substring(OVERRIDE_ENV_PREFIX.length());
        if (!flagKey.isEmpty()) {
            overrides.put(flagKey, entry.getValue());
        }
    }

    return overrides;
}

private static Map<String, Flag<?>> buildInMemoryFlags(Map<String, Object> overrides) {
    Map<String, Flag<?>> flags = new HashMap<>();

    for (Map.Entry<String, Object> entry : overrides.entrySet()) {
        Object value = entry.getValue();
        Flag.Builder<?> builder = Flag.builder().defaultVariant("forced");

        if (value instanceof Boolean boolValue) {
            builder.variant("forced", boolValue);
        } else if (value instanceof Number numberValue) {
            builder.variant("forced", numberValue.doubleValue());
        } else {
            builder.variant("forced", value.toString());
        }

        flags.put(entry.getKey(), builder.build());
    }

    return flags;
}

public static void initializeFeatureFlags() throws Exception {
    InMemoryProvider overrideProvider = new InMemoryProvider(buildInMemoryFlags(loadOverrides()));
    Provider datadogProvider = new Provider();

    MultiProvider multiProvider = new MultiProvider(
        List.of(overrideProvider, datadogProvider)
    );

    OpenFeatureAPI api = OpenFeatureAPI.getInstance();
    api.setProviderAndWait(multiProvider);
}
```

{% /tab %}

Set `FF_checkout_new=true` and `FF_ui_theme=dark` in the environment before starting the process to force those variants.

## Evaluate flags{% #evaluate-flags %}

After initialization, evaluate flags through the OpenFeature client as usual. Overridden flags return the forced value; all other flags resolve from Datadog.

{% tab title="Browser" %}

```javascript
const client = OpenFeature.getClient();
const isEnabled = client.getBooleanValue('checkout_new', false);
```

{% /tab %}

{% tab title="Server" %}

```javascript
const client = OpenFeature.getClient();
const isEnabled = await client.getBooleanValue('checkout_new', false, {
  targetingKey: req.session?.userID ?? 'unknown',
});
```

{% /tab %}

## Further reading{% #further-reading %}

Additional helpful documentation, links, and articles:

- [Server-Side Feature Flags](https://docs.datadoghq.com/feature_flags/server.md)
- [JavaScript Feature Flags](https://docs.datadoghq.com/feature_flags/client/javascript.md)
- [Browser Developer Extension](https://docs.datadoghq.com/feature_flags/browser_developer_extension.md)
- [Multi-Provider Specification](https://openfeature.dev/specification/appendix-a/#multi-provider)
- [OpenFeature Multi-Provider Release](https://openfeature.dev/blog/openfeature-multi-provider-release/)
