Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

The availability of features may depend on your plan type. Contact your Customer Success Manager if you have any questions.

Dev guideRecipesAPI ReferenceChangelog
Dev guideAPI ReferenceUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Flag dependency

Overview and implementation details of flag dependency in Optimizely Feature Experimentation.

Using flag dependency, you can control a user's exposure to certain feature flags based on whether they would be exposed to other "parent" flags. This lets you enforce dependencies between features, ensuring that users only see a particular feature if they qualify for another related feature.

Optimizely Feature Experimentation SDKs use a deterministic algorithm to bucket users to variations. This means you can evaluate whether a user would have been exposed to a parent flag, even if Optimizely has not made the decision yet. This approach lets you build dependencies without persisting user state.

📘

Note

This document does not cover the use case where you show a flag only to users who have already encountered a parent flag. Implementing that behavior requires storing state and is outside the scope of this documentation.

Usage note

Variables prefixed with _ in this document are defined at the feature flag level, not at the variation level. These variables set dependency rules for child flags and should remain unchanged within variations. They act as static rules determining whether a child flag is evaluated based on its parent flags.

This approach complements Optimizely Feature Experimentation's powerful rules engine rather than replacing it. While you define dependencies at the flag level, the final user experience is controlled by the targeted delivery or experiment rules configured in both the parent and child flags. This method works seamlessly for both feature flag dependencies and experiment dependencies.

Traditional dependency

Traditional dependency exposes a user to a flag if that user would be exposed to a parent flag.

For example, you create two flags for a new landing page.

  • new_landing_page – Flag for the new landing page.
  • new_cta – Flag for a call to action (CTA) on the landing page.

Users should only see the new CTA if they qualify for the new landing page. The new_cta flag is evaluated if the new_landing_page flag is enabled for the user. This creates a dependency where the CTA flag relies on the new landing page flag.

The child flag should include a special variable called _depends_on_parents of type String. This variable is a comma-delimited list of flags on which the child depends. For example, the _depends_on_parent variable is set to new_landing_page.

📘

Note

If multiple parents are specified, add a boolean variable, _depends_evaluate_all_parents. If set to true, the code evaluates all parents to true before evaluating the dependent. If false, the code evaluates the dependent flag if any parent evaluates to true.

Example code

const decideWithDependencies = function(userContext, flagKey, options) {
    if (_optimizelyClient == null) {
        return;
    }

    const config = _optimizelyClient.getOptimizelyConfig();
    let _dependencyDecisions = [];
    let dependencyMetadata = "";

    // Check dependencies
    if (config.featuresMap.hasOwnProperty(flagKey)) {
        var flag = config.featuresMap[flagKey];

        if (flag.variablesMap.hasOwnProperty("_depends_on_parents")) {
            let evaluateAllParents = true;
            let parentDecisionsEnabled = 0;

            if (flag.variablesMap.hasOwnProperty("_depends_evaluate_all_parents")) {
                evaluateAllParents = flag.variablesMap._depends_evaluate_all_parents.value;
            }

            let dependencies = flag.variablesMap._depends_on_parents.value.split(/[, ]+/);

            // Evaluate each dependency
            dependencies.some(function(dependency, index) {
                // If the dependency is not enabled, force the "off" variation to be returned
                const decision = userContext.decide(dependency, {
                    DISABLE_DECISION_EVENT: true
                });
                _dependencyDecisions.push(decision);

                if (decision.enabled) {
                    // Parent decision is enabled.
                    parentDecisionsEnabled += 1;
                    if (!evaluateAllParents) {
                        // If we do not require dependency on all parents, we can exit out of the loop
                        return;
                    }
                }
            });

            if (
                (evaluateAllParents && parentDecisionsEnabled != dependencies.length) ||
                (!evaluateAllParents && parentDecisionsEnabled == 0)) {
                userContext.setForcedDecision({
                    flagKey: flagKey
                }, {
                    variationKey: "off"
                });
            }
        }
    }

    // Make the decision for flagKey
    // If flagKey has any disabled dependencies, decide() returns "off"
    let decision = userContext.decide(flagKey, options);
    decision.dependencyDecisions = _dependencyDecisions;
    // Remove the forced decision rule
    userContext.removeForcedDecision({
        flagKey: flagKey,
        ruleKey: null
    });

    return decision;
};

The previous code sample completes the following:

  1. Retrieve the Optimizely client configuration.
  2. Check if the requested flag exists in the configuration.
  3. Determine if the flag has a _depends_on_parents variable.
  4. Evaluate each parent flag and determine if the child flag should be enabled.

    📘

    Note

    The decision event is disabled because this process only checks if the user would be exposed to the parent flag.

  5. Make a decision on the child flag based on dependencies.
  6. Store dependency decision for future reference.

Inverse activation

In some cases, you may want to enforce the opposite logic of traditional dependency, where a child flag is disabled if a parent flag is enabled. This is useful when certain features should be mutually exclusive.

For example, create the following flags:

  • mobile-only-experience
  • desktop-only-experience
  • combined-mobile-and-desktop-experience

Users assigned to mobile-only-experience should not be assigned to desktop-only-experience, and vice versa. However, users in combined-mobile-and-desktop-experience should not be assigned to either individual experience. In this case, you need a dependency rule that ensures mutual exclusivity between the combined experience and the individual experiences.

To accomplish this, create a flag-level variable, _inverse_activation. By default, _inverse_activation is false, which results in the traditional behavior. When _inverse_activation set to true, it inverts the dependency logic.

Inverse activation code

The updated code retrieves the _inverse_activation variable and modifies the dependency check accordingly.

let inverseActivation = false;

if (flag.variablesMap.hasOwnProperty("_inverse_activation")) {
    inverseActivation = flag.variablesMap._inverse_activation.value == "true";
}

if (
    (inverseActivation && !decision.enabled) ||
    (!inverseActivation && decision.enabled)) {
    // Parent decision is enabled.
    parentDecisionsEnabled += 1;
    if (!evaluateAllParents) {
        // If we do not require dependency on all parents, we can exit out of the loop
        return;
    }
}

The completed code is included in the following code sample:

const decideWithDependencies = function(userContext, flagKey, options) {
    if (_optimizelyClient == null) {
        return;
    }

    const config = _optimizelyClient.getOptimizelyConfig();
    const _dependencyDecisions = [];
    let dependencyMetadata = "";

    // Check dependencies
    if (config.featuresMap.hasOwnProperty(flagKey)) {
        var flag = config.featuresMap[flagKey];

        if (flag.variablesMap.hasOwnProperty("_depends_on_parents")) {
            let evaluateAllParents = true;
            let parentDecisionsEnabled = 0;

            if (flag.variablesMap.hasOwnProperty("_depends_evaluate_all_parents")) {
                evaluateAllParents = flag.variablesMap._depends_evaluate_all_parents.value === 'true';
            }

            let dependencies = flag.variablesMap._depends_on_parents.value.split(/[, ]+/);
            let inverseActivation = false;

            if (flag.variablesMap.hasOwnProperty("_inverse_activation")) {
                inverseActivation = flag.variablesMap._inverse_activation.value == "true";
            }

            // Evaluate each dependency
            dependencies.some(function(dependency, index) {
                // If the dependency is not enabled, force the "off" variation to be returned
                const decision = userContext.decide(dependency, {
                    DISABLE_DECISION_EVENT: true
                });
                _dependencyDecisions.push(decision);

                if (
                    (inverseActivation && !decision.enabled) ||
                    (!inverseActivation && decision.enabled)) {
                    // Parent decision is enabled.
                    parentDecisionsEnabled += 1;
                    if (!evaluateAllParents) {
                        // If we do not require dependency on all parents, we can exit out of the loop
                        return;
                    }
                }
            });

            if (
                (evaluateAllParents && parentDecisionsEnabled != dependencies.length) ||
                (!evaluateAllParents && parentDecisionsEnabled == 0)) {
                userContext.setForcedDecision({
                    flagKey: flagKey
                }, {
                    variationKey: "off"
                });
            }
        }
    }

    // Make the decision for flagKey
    // If flagKey has any disabled dependencies, decide() will return "off"
    let decision = userContext.decide(flagKey, options);
    decision.dependencyDecisions = _dependencyDecisions;
    // Remove the forced decision rule
    userContext.removeForcedDecision({
        flagKey: flagKey,
        ruleKey: null
    });

    return decision;
};

Bind to the SDK

By default, feature flag decisions are made directly on the user context object. To integrate the decideWithDependencies function into the SDK, bind it to the Optimizely client during initialization.

_optimizelyClient.decideWithDependencies = decideWithDependencies.bind(_optimizelyClient);

Then, use the following function whenever decide is currently called:

const decision = _optimizelyClient.decideWithDependencies(userContext, flagKey, options);

This ensures that the code evaluates dependencies before deciding on feature exposure.