Skip to content

Example: Feature Flags & Version Gates

This example shows how to use Mustang to implement sophisticated feature flag rules and version gates — conditions based on app version, user tier, device, and geography.


Scenario

A mobile app backend controls feature availability using Boolean rules:

Feature Flag Rule
new-checkout-ui appVersion ≥ 4.0.0 AND platform ∈
dark-mode-beta tier ∈ {premium, beta} AND appVersion ≥ 3.5.0
upi-autopay country ∈ {IN} AND appVersion ≥ 3.2.0
legacy-warning appVersion < 2.0.0
ab-bucket-0 userId % 10 == 0 (10% rollout, bucket 0)

Setup

import com.phonepe.mustang.detail.impl.VersioningDetail;
import com.phonepe.mustang.detail.impl.VersioningDetail.CheckType;
import com.phonepe.mustang.preoperation.impl.ModuloPreOperation;

MustangEngine engine = MustangEngine.builder().mapper(mapper).build();

// new-checkout-ui: appVersion >= 4.0.0 AND platform ∈ {android, ios}
Criteria newCheckout = DNFCriteria.builder()
    .id("new-checkout-ui")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.appVersion")
            .detail(VersioningDetail.builder()
                .check(CheckType.ABOVE)
                .baseVersion("4.0.0")
                .build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.platform")
            .detail(EqualityDetail.builder()
                .values(Sets.newHashSet("android", "ios"))
                .build())
            .build())
        .build())
    .build();

// dark-mode-beta: tier ∈ {premium, beta} AND appVersion >= 3.5.0
Criteria darkMode = DNFCriteria.builder()
    .id("dark-mode-beta")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.tier")
            .detail(EqualityDetail.builder()
                .values(Sets.newHashSet("premium", "beta"))
                .build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.appVersion")
            .detail(VersioningDetail.builder()
                .check(CheckType.ABOVE)
                .baseVersion("3.5.0")
                .build())
            .build())
        .build())
    .build();

// upi-autopay: country=IN AND appVersion >= 3.2.0
Criteria upiAutopay = DNFCriteria.builder()
    .id("upi-autopay")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.country")
            .detail(EqualityDetail.builder().values(Sets.newHashSet("IN")).build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.appVersion")
            .detail(VersioningDetail.builder()
                .check(CheckType.ABOVE)
                .baseVersion("3.2.0")
                .build())
            .build())
        .build())
    .build();

// legacy-warning: appVersion < 2.0.0
Criteria legacyWarning = DNFCriteria.builder()
    .id("legacy-warning")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.appVersion")
            .detail(VersioningDetail.builder()
                .check(CheckType.BELOW)
                .baseVersion("2.0.0")
                .excludeBase(true)   // strictly below 2.0.0
                .build())
            .build())
        .build())
    .build();

// ab-bucket-0: userId % 10 == 0
Criteria abBucket0 = DNFCriteria.builder()
    .id("ab-bucket-0")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.userId")
            .preOperation(ModuloPreOperation.builder().rhs(10).build())
            .detail(EqualityDetail.builder().values(Sets.newHashSet(0)).build())
            .build())
        .build())
    .build();

engine.add("features", List.of(newCheckout, darkMode, upiAutopay, legacyWarning, abBucket0));

Check features for a user request

JsonNode request = mapper.readTree("""
    {
      "userId": 120,
      "country": "IN",
      "platform": "android",
      "appVersion": "4.1.0",
      "tier": "premium"
    }
    """);

RequestContext ctx = RequestContext.builder().node(request).build();
Set<String> enabledFeatures = engine.search("features", ctx);
// → ["new-checkout-ui", "dark-mode-beta", "upi-autopay", "ab-bucket-0"]
//
// Explanation:
//   new-checkout-ui:  4.1.0 >= 4.0.0 ✓, platform=android ✓   → ENABLED
//   dark-mode-beta:   tier=premium ✓, 4.1.0 >= 3.5.0 ✓        → ENABLED
//   upi-autopay:      country=IN ✓, 4.1.0 >= 3.2.0 ✓          → ENABLED
//   legacy-warning:   4.1.0 < 2.0.0 ✗                         → DISABLED
//   ab-bucket-0:      120 % 10 = 0 ✓                           → ENABLED

Percentage rollouts with PreOperations

Use ModuloPreOperation to implement arbitrary rollout percentages:

// 25% rollout — buckets 0-24 out of 100
Criteria rollout25 = DNFCriteria.builder()
    .id("new-payment-flow-25pct")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.userId")
            .preOperation(ModuloPreOperation.builder().rhs(100).build())
            .detail(RangeDetail.builder()
                .lowerBound(0).includeLowerBound(true)
                .upperBound(25).includeUpperBound(false)
                .build())
            .build())
        .build())
    .build();

Locale-based feature gates with SubString

Target specific country codes from a locale string like "en-IN":

import com.phonepe.mustang.preoperation.impl.SubStringPreOperation;

// Gate on country code extracted from locale (e.g., "en-IN" → "IN")
Criteria indiaGate = DNFCriteria.builder()
    .id("india-locale-feature")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.locale")
            .preOperation(SubStringPreOperation.builder()
                .beginIndex(3)
                .endIndex(5)
                .build())
            .detail(EqualityDetail.builder()
                .values(Sets.newHashSet("IN"))
                .build())
            .build())
        .build())
    .build();

Time-based gates with DateTimePreOperation

Enable a feature only during business hours (9am–6pm):

import com.phonepe.mustang.preoperation.impl.DateTimePreOperation;
import com.phonepe.mustang.preoperation.impl.DateExtractionType;

Criteria businessHours = DNFCriteria.builder()
    .id("business-hours-only")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.requestTime")   // epoch millis in the event
            .preOperation(DateTimePreOperation.builder()
                .extractionType(DateExtractionType.HOUR_OF_DAY)
                .build())
            .detail(RangeDetail.builder()
                .lowerBound(9).includeLowerBound(true)
                .upperBound(18).includeUpperBound(false)
                .build())
            .build())
        .build())
    .build();