Skip to content

Ship dd-openfeature as a standalone SDK extended by the Java agent (AI prototype) - #12709

Draft
PerfectSlayer wants to merge 100 commits into
masterfrom
bbujon/ffe
Draft

PerfectSlayer wants to merge 100 commits into
masterfrom
bbujon/ffe

Conversation

@PerfectSlayer

@PerfectSlayer PerfectSlayer commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Warning

AI-generated prototype, not human reviewed.
Claude Code generated all the code, tests and docs in this PR from a design brief. No human has reviewed any of it yet. It is a draft to discuss the design proposal, not a merge candidate. Review it as a design sketch and expect rough edges.

What Does This Do

This prototypes a different architecture for running Datadog Feature Flags without dd-java-agent. It covers the same need as #12576, but under different constraints.

  • dd-openfeature is a standalone SDK in a new root module, :dd-openfeature (JDK 11+), published as com.datadoghq:dd-openfeature.
    • It uses no dd-trace-java internal module.
    • Its only dependencies are the OpenFeature SDK, the OpenTelemetry API and jackson-core, all as plain POM dependencies.
    • It talks to the network through java.net.http.HttpClient.
  • dd-java-agent is optional. When attached, it connects to the SDK through library instrumentation. The SDK then uses the agent's Remote Configuration, its Datadog Agent event proxy, agent telemetry and APM span enrichment.
  • The application always adds the SDK and registers the provider. The agent never installs a provider by itself.
App classloader                                     │ Bootstrap CL              │ Agent CL
────────────────────────────────────────────────────┼───────────────────────────┼─────────────────────────
dd-openfeature (SDK)                                │                           │
  Provider ─► FeatureFlagsRuntime ─► Connectors.detect() ◄── advice (returns JavaAgentConnector)
     │          ├─ configuration: CDN poller │ connector Remote Configuration
     │          ├─ events: direct intake │ connector event proxy (+ fallback)
     │          └─ health metrics / span enrichment: no-op │ connector
  JavaAgentConnector (injected helper) ─────────────► FeatureFlaggingGateway ◄─ FeatureFlaggingSystem
                                                    │  (JDK types only)         │  RC FFE_FLAGS (raw bytes),
                                                    │                           │  EVP proxy, telemetry,
                                                    │                           │  span enrichment

Motivation

The design brief ("[DRAFT] FFE Design Doc", following the product brief Java Feature Flags: organize as standalone and SSI) rejects the shape of #12576 for two reasons:

  1. It turns internal dd-trace-java modules into customer-facing contracts. That slows down the modularization of dd-trace-java.
  2. It bundles internal modules and their dependencies onto the application classpath. That causes hard-to-diagnose conflicts and exposes our internals.

The brief also notes the following:

  • No precedent for auto-installation. Neither the OpenFeature documentation nor any competitor (LaunchDarkly, Split, DevCycle, Statsig, Optimizely, Flagsmith, ConfigCat, Unleash, GrowthBook, flagd) installs a provider without code.
  • Injection is off by default anyway. The FFE team doesn't want injection enabled by default, which defeats the SSI case.

So the install path follows OpenFeature practice: always add the SDK, and optionally attach the agent.

Design differences with #12576

Topic #12576 This PR
SDK packaging Shadow jar bundling internal-api, communication, config-utils, OkHttp, Moshi, JCTools…, relocated and shrunk with minimize() (~1.84 MB) Plain jar, about 154 KB. POM dependencies are only dev.openfeature:sdk, io.opentelemetry:opentelemetry-api and jackson-core. No shading, no minimize()
HTTP and JSON Relocated OkHttp and Moshi java.net.http.HttpClient and jackson-core streaming (no old HTTP client for security scanners to flag)
SDK ↔ agent contract Public bootstrap POJOs (UFC model, ExposureEvent, FlagEvalEvent) on FeatureFlaggingGateway, plus an AGENT/STANDALONE runtime ownership claim An SPI owned by the SDK (com.datadog.openfeature.internal.connector). Instrumentation exit advice on Connectors.detect() fills it in. The bootstrap gateway is internal to the agent, uses JDK types only and can change freely
Version skew Reflection probes and LinkageError catches Muzzle checks the helpers' references to the SPI. On a mismatch the instrumentation doesn't apply and the SDK runs standalone
Provider installation The agent installs the provider when OpenFeatureAPI.getInstance() is called (openfeature-1.20 instrumentation) No auto-installation: the application registers new Provider()
Evaluation and UFC parsing Shared core package; the agent parses with Moshi; the evaluator is injected as helpers Everything in the SDK. The agent forwards raw Remote Configuration documents and event payloads
Remote Configuration without the agent Optional dd-openfeature-remote-config artifact (~4.17 MB, bundles remote-config-core) Out of scope for now (// TODO). remote_config requires the agent
Injected helper conflicts Different copies of evaluator helpers from the agent and the SDK on the same classpath Helpers only implement the SDK's SPI interfaces, so no SDK class is duplicated
Public API datadog.trace.api.openfeature.Provider com.datadog.openfeature.Provider. The old class name stays as a deprecated subclass. The SDK can't sit under datadog.trace.*, which the agent never instruments and loads bootstrap-first
Older SDKs on a new agent Kept working through bridge probes Dropped (see breaking change below)

Changes

  • :dd-openfeature (new root module), ported from feature-flagging-api/-lib and Prototype standalone and injected Java Feature Flags together #12576:
  • dd-openfeature-connector instrumentation (dd-java-agent/instrumentation/datadog/openfeature/):
    • Exit advice on Connectors.detect().
    • Gated by a new TargetSystem.FEATURE_FLAGS, which follows the resolved DD_FEATURE_FLAGS_* settings.
    • Helpers are compiled in a java11 source set.
    • Integration toggle DD_TRACE_OPENFEATURE_ENABLED (aliases DD_TRACE_INTEGRATION_OPENFEATURE_ENABLED, DD_INTEGRATION_OPENFEATURE_ENABLED), declared in metadata/supported-configurations.json.
  • Agent side (products/feature-flagging):
    • The bootstrap module is reduced to a FeatureFlaggingGateway.Backend registry.
    • FeatureFlagsBackend provides:
      • Remote Configuration fan-out of raw FFE_FLAGS documents. The product is registered at agent startup in remote_config mode, so the first poll asks for it.
      • The event platform proxy client, with fallback signalling for the SDK.
      • Telemetry counters.
      • Span enrichment through the existing writer.
    • FeatureFlaggingConfig only keeps the product switch and the configuration source. The CDN, span-enrichment and evaluation-count keys are read by the SDK only.
  • Removed:
    • The feature-flagging-api module, and the agent-side CDN poller, parsers, event writers and bootstrap POJOs.
    • The agent's CDN-related Config getters, which the SDK now owns.
  • CI:
    • The publication and CI jar paths are switched to :dd-openfeature.
    • The system-tests workflow builds dd-openfeature and ships it next to the agent jar, as system tests would otherwise use the published SDK (see breaking change).
  • Smoke tests (dd-smoke-tests/openfeature) now cover three setups:
    • agent + Remote Configuration
    • agent + CDN, the default source
    • no agent + CDN
  • Docs: dd-openfeature/README.md.

Features by setup

Feature SDK only SDK + dd-java-agent
Configuration (agentless, the default) CDN CDN
Configuration (remote_config) Not supported (init error) Remote Configuration
Exposures / evaluation counts Direct intake (DD_API_KEY) Event proxy; direct intake fallback in agentless mode
feature_flag.evaluations OTel metric Yes Yes
Health metrics, span enrichment No Yes

Breaking change

dd-openfeature 1.66.0 and earlier link against bootstrap types this PR removes, so they no longer work with this agent. dd-openfeature and dd-java-agent must be upgraded together. This was a deliberate choice to avoid keeping the legacy bridge. For the same reason, system tests now run with the dd-openfeature jar built from this branch.

Testing

All suites below were run locally on this branch. The smoke tests and ConfigTest ran on an earlier commit of it.

Suite Result
:dd-openfeature:test + forkedTest 781 passed
feature-flagging-lib / -agent / -bootstrap / -config 72 / 3 / 1 / 2 passed
dd-openfeature-connector instrumentation (test + forkedTest), including the agent/SDK configuration parity test 25 passed
:dd-smoke-tests:openfeature (3 setups) 932 passed, 3 skipped (agent telemetry checks in the no-agent setup)
internal-api ConfigTest passed

Not covered:

  • Muzzle: the pass directive can't exist until the SDK is published (// TODO).
  • SDK coverage: about 17 :dd-openfeature classes are below the 90% per-class coverage threshold. CI doesn't enforce it for this module, as coverage is only checked on Java 8.
  • Benchmarks: JMH benchmarks from the removed modules are not ported (// TODO).
  • Other environments: no SSI or real-backend validation.
  • Pre-PR reviews: /techdebt and /perf-review have not been run.

Open questions

  • Is dropping support for older dd-openfeature versions on a new agent acceptable, or do we need a deprecation window?
  • jackson-core as a plain dependency: what minimum version do we support against customer BOMs?
  • Remote Configuration without the agent (for example through libdatadog), and trace enrichment through the OpenTelemetry SDK, are both left as follow-ups.

Additional Notes

Jira ticket: APMLP-1512

🤖 Generated with Claude Code

@PerfectSlayer PerfectSlayer added type: feature Enhancements and improvements tag: ai generated Largely based on code generated by an AI or LLM comp: openfeature OpenFeature labels Oct 1, 2026
@datadog-datadog-prod-us1

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.94 s 13.90 s [-0.4%; +0.9%] (no difference)
startup:insecure-bank:tracing:Agent 12.91 s 12.97 s [-1.2%; +0.5%] (no difference)
startup:petclinic:appsec:Agent 17.68 s 17.60 s [-0.5%; +1.3%] (no difference)
startup:petclinic:iast:Agent 17.58 s 17.54 s [-0.7%; +1.1%] (no difference)
startup:petclinic:profiling:Agent 17.41 s 16.85 s [-1.2%; +7.9%] (no difference)
startup:petclinic:sca:Agent 17.72 s 17.54 s [-0.1%; +2.2%] (no difference)
startup:petclinic:tracing:Agent 16.65 s 16.69 s [-1.3%; +0.8%] (no difference)

Commit: 5c623497 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

Port #12575 to the SDK and the agent bridge: exposures and flag
evaluations carry the DD-EVP-ORIGIN and DD-EVP-ORIGIN-VERSION headers,
dd-trace-java and the library version, through the agent event proxy and
direct intake alike. An agentless proxy endpoint is used only when the
Agent forwards both headers. The smoke tests mock Agent advertises them
like the Agent does.
Upload the agent and dd-openfeature jars from a dedicated folder instead
of copying dd-openfeature into the agent build output.
PerfectSlayer and others added 22 commits October 22, 2026 10:11
Add the deprecated datadog.trace.api.openfeature.ExposureHook, and its
Evaluation type, delegating to com.datadog.openfeature.ExposureHook:
master documents the hook under its former package name.
The build job keeps the dd-openfeature jar: also preserve it as a
validate_build reference, and enable its compareToReferenceJar check.
Master removed the Gradle IDEA plugin configuration: the Java toolchain
and compiler settings already give IntelliJ the right JDK.
Replace the legacy provider instrumentation tests, which loaded a
released provider in a dedicated class loader, with a smoke test running
an application built with the released dd-openfeature 1.67.1, with and
without Feature Flags enabled. Drop the IDEA module configuration and use
the static VERSION_11 import in the legacy provider instrumentation build.
Mark the instrumentation module and advice classes as unused, as they are
only referenced through AutoService and advice class names, and import
the Advice annotations statically.
Move the agent bridge SPI to a versioned package, bridge.v1: once
released, a version is frozen and only gets default methods, and a
breaking change goes to a new package. Check each released SPI version
against the first SDK release that shipped it with japicmp, failing on
any change breaking the agent implementation. The v1 baseline is set
once the first SDK release with the agent bridge is published.
Check the API outside the internal packages with japicmp against the
previous dd-openfeature release, failing on binary or source
incompatible changes. The baseline is the latest release before the
version being built, so release branches compare against their own line
and each release keeps the API of all the previous ones.
…y errors

The SDK dropped malformed Remote Configuration documents and the agent
swallowed subscriber failures, so the poller acknowledged documents the
SDK never applied. The SDK now rejects malformed documents by throwing,
and the agent rethrows the first subscriber failure once every subscriber
is notified, so the poller reports an apply error as before the switch.
…e Flags

The poller reference-counts start and stop calls, but closing the Feature
Flags Remote Configuration service no longer stopped it, keeping it polling
once Feature Flags closed and leaking a reference on each restart.
…e SDK

The bridge was only installed when the agent enabled Feature Flags, so
when only an agent configuration source the SDK cannot read (stable
configuration, configuration file) disabled them, the SDK fell back to
its own settings and started agentless. The bridge is now installed
regardless, and reports Feature Flags as disabled when the agent
configuration disables them.
…on is disabled

The Feature Flags shutdown hook was only registered when tracing was
disabled, but the tracing shutdown path also relies on the shutdown
instrumentation, which can be disabled on its own. The hook is now always
registered once the SDK subscribes to the shutdown; stopping is idempotent,
so it does nothing after the tracing shutdown path already stopped Feature
Flags.
The test rewrote the SPI classes to check the runtime SPI guard. The
japicmp bridge SPI check covers SPI compatibility once the SPI is
released.
Once dd-openfeature 1.69.0 is released, the japicmpBridgeV1 task checks
the frozen bridge.v1 SPI, so the runtime check of the helpers becomes
redundant.
…0 release

Reference APMLP-1512 in the TODOs waiting for the first SDK release with
the agent bridge, and prepare the muzzle pass directive of the bridge
instrumentation, to enable once 1.69.0 is published.
The agent bridge ships with dd-java-agent 1.69.0, and the providers
released before 1.69.0 return the caller defaults with it.
Remote Configuration without the Datadog Java agent is out of scope.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: openfeature OpenFeature tag: ai generated Largely based on code generated by an AI or LLM tag: override groovy enforcement Override the "Enforce Groovy Migration" check type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants