Appendix H: Feature Flags

Flagd + OpenFeature with Camel routes — gradual rollouts, A/B testing, and runtime route control.

15 minutesChapter 26appendices

The Detour pattern (Chapter 18) used configuration properties to toggle processing steps. Feature flags extend this to dynamic, fine-grained control: roll out a new enrichment step to 10% of traffic, enable hazmat routing only for specific customer tiers, or A/B test two content-based routing strategies.

The code is in examples/26-feature-flags/, with Quarkus and Spring Boot variants and the flag definitions in examples/_infra/flagd/flags.json.

# Quarkus
cd examples/26-feature-flags/quarkus
mvn quarkus:dev
# Spring Boot
cd examples/26-feature-flags/spring-boot
mvn spring-boot:run

flagd runs as part of the base stack, so ./scripts/setup-stack.sh is all the infrastructure you need. It watches the flag file, which means editing flags.json changes behaviour on the very next message — no rebuild, no restart, no redeploy. Run the example and try it; that property is the whole argument for feature flags and it is much more convincing seen than described.

OpenFeature + flagd

OpenFeature is a vendor-neutral API for feature flag evaluation. flagd is a lightweight flag evaluation daemon that reads flag definitions from a file or ConfigMap.

Architecture

flagd architecture
Figure Q.1 — flagd evaluation with OpenFeature SDK

Flag definitions

{
  "flags": {
    "enrichment-enabled": {
      "state": "ENABLED",
      "variants": {
        "on": true,
        "off": false
      },
      "defaultVariant": "on",
      "targeting": {}
    },
    "new-routing-algorithm": {
      "state": "ENABLED",
      "variants": {
        "legacy": "content-based-router",
        "new": "dynamic-router"
      },
      "defaultVariant": "legacy",
      "targeting": {
        "fractional": [
          ["legacy", 90],
          ["new", 10]
        ]
      }
    },
    "hazmat-compliance-v2": {
      "state": "ENABLED",
      "variants": {
        "v1": false,
        "v2": true
      },
      "defaultVariant": "v1",
      "targeting": {
        "if": [
          { "in": ["$customer_tier", ["ENTERPRISE", "VIP"]] },
          "v2",
          "v1"
        ]
      }
    }
  }
}

Integration with Camel

@ApplicationScoped
@Named("featureFlags")
public class FeatureFlagEvaluator {

    @Inject
    Client openFeatureClient;

    public boolean isEnabled(String flagKey) {
        return openFeatureClient.getBooleanValue(flagKey, false);
    }

    public String getVariant(String flagKey, Map<String, Object> context) {
        MutableContext evalContext = new MutableContext();
        context.forEach((k, v) -> evalContext.add(k, String.valueOf(v)));
        return openFeatureClient.getStringValue(flagKey, "default", evalContext);
    }
}

// Detour controlled by feature flag
from("kafka:eip.orders.placed?brokers=localhost:9092&groupId=flag-router")
    .routeId("feature-flag-detour")
    .unmarshal().json(Map.class)
    .process(exchange -> {
        boolean enrichmentEnabled = exchange.getContext()
            .getRegistry().lookupByNameAndType("featureFlags", FeatureFlagEvaluator.class)
            .isEnabled("enrichment-enabled");
        exchange.getIn().setHeader("enrichmentEnabled", enrichmentEnabled);
    })
    .choice()
        .when(header("enrichmentEnabled").isEqualTo(true))
            .to("direct:enrich-order")
        .otherwise()
            .log("Enrichment disabled by feature flag")
    .end()
    .to("direct:process-order");

// A/B test routing algorithm
from("kafka:eip.orders.placed?brokers=localhost:9092&groupId=ab-test")
    .routeId("feature-flag-ab-test")
    .unmarshal().json(Map.class)
    .process(exchange -> {
        Map<String, Object> order = exchange.getIn().getBody(Map.class);
        FeatureFlagEvaluator flags = exchange.getContext()
            .getRegistry().lookupByNameAndType("featureFlags", FeatureFlagEvaluator.class);
        String algorithm = flags.getVariant("new-routing-algorithm",
            Map.of("customer_tier", String.valueOf(order.get("customer_tier"))));
        exchange.getIn().setHeader("routingAlgorithm", algorithm);
    })
    .toD("direct:${header.routingAlgorithm}");

Running flagd in the Podman stack

Add flagd to compose.yaml:

flagd:
  image: ghcr.io/open-feature/flagd:v0.16.3
  container_name: eip-flagd
  ports:
    - "8013:8013"   # gRPC evaluation
    - "8014:8014"   # metrics / health
  volumes:
    - ./flagd/flags.json:/etc/flagd/flags.json:Z
  command: start --uri file:/etc/flagd/flags.json
  mem_limit: 256m
  networks:
    - eip-net

Pin the version, and pin it above v0.16. The Java provider 0.14.2 cannot establish its event stream against flagd v0.13.1. The failure is quiet in the worst way: evaluation over the Connect RPC still works if you curl it by hand, but the provider never reaches ready, so the SDK returns the default for every flag and your application looks like it is simply ignoring the flag file. flagd is distroless, so it has no in-container healthcheck — setup-stack.sh polls http://localhost:8014/healthz from the host instead.

Dependencies

<dependency>
    <groupId>dev.openfeature</groupId>
    <artifactId>sdk</artifactId>
    <version>1.22.1</version>
</dependency>
<dependency>
    <groupId>dev.openfeature.contrib.providers</groupId>
    <artifactId>flagd</artifactId>
    <version>0.14.2</version>
</dependency>

On Quarkus you also need camel-quarkus-bean: the flag lookup is a bean-backed expression, and Quarkus resolves the bean language only when that extension is present. Without it the route fails at startup with No language could be found for: bean, which does not obviously point at the cause.

On Spring Boot, set camel.main.run-controller=true. Without a web starter there is nothing else holding the JVM open, and the application shuts down a few seconds after the routes start. Note the property name — camel.main., not camel.springboot.; the latter is silently ignored.


Verification status: verifiedexamples/26-feature-flags/ was run on both runtimes against flagd v0.16.3 in the Podman stack on 2026-09-15. The detour enriches while enrichment-enabled is on; ENTERPRISE and VIP orders resolve hazmat-compliance-v2 to v2 while STANDARD gets v1; and the fractional flag split 31 orders 29/2 between the legacy and new routing algorithms, consistent with its 90/10 targeting.