Under the hood¶
ZIO OpenFeature is a ZIO layer over the OpenFeature Java SDK.
The SDK holds the provider and talks to it. FeatureFlags adds what the SDK does not have: effects and typed errors,
fiber-local context, ZIO hooks, transactions, a provider status you can rely on, and events as a stream.
The pieces¶
| Piece | What it is | Where it lives |
|---|---|---|
FeatureFlags |
The service your code calls | a ZLayer, built by one of the factories |
OpenFeatureAPI |
The SDK's registry of providers and API-level hooks | the global singleton, or an isolated instance per FeatureFlagRegistry |
Client |
The SDK object that evaluates flags | one per FeatureFlags, internal |
FeatureProvider |
Any OpenFeature Java provider | passed to the factory |
EvaluationContext |
Targeting attributes | an immutable Scala value, converted to the SDK's type per call |
FeatureHook |
A ZIO hook | run by FeatureFlags on your fiber |
ProviderEvent |
Provider readiness and change notifications | published on a ZIO Hub |
The OpenFeature concepts map one to one: the SDK's Hook becomes FeatureHook, its EvaluationContext becomes
ours, its event handlers become onProviderReady, onConfigurationChanged and FeatureFlags.events.
One evaluation, step by step¶
- Context. Six levels merge into one context, each overriding the ones before it: global, transaction, client,
context source, scoped (
withContext), then the context passed to the call. See Evaluation context. - Before hooks run in order: API-level ZIO hooks, then client hooks, then the call's own hooks. Each can add context, which lands on top of everything else. See Hooks.
- Transaction. Inside a transaction, an override for the key answers at once with reason
CACHED. With caching on, so does a flag the transaction has already read. The provider is not called. - Status gate. If the provider is
NotReady,ShuttingDownorFatal, the evaluation fails fast withProviderNotReadyorProviderFatal.Ready,StaleandErrorgo through. See Lifecycle and events. - Provider. The SDK client is called on ZIO's blocking pool, with a timeout (1 second by default). On timeout the evaluation fails, but the provider call itself is abandoned, not cancelled. It holds its thread until it returns, so each instance caps the abandoned calls still running (256 by default) and rejects timed evaluations at the cap; see Evaluating flags.
- After or error hooks run in reverse order. A resolution that carries an error code (
FLAG_NOT_FOUND,TYPE_MISMATCH, …) runs the error stage, not the after stage. - finallyAfter hooks run last, whatever happened.
- Your answer. The typed tier turns an error code into a
FeatureFlagError. The total tier serves your default and keeps the code on the result. See Evaluating flags.
Hooks run on every path, including overrides, cache hits and a provider that is not ready: a before hook sees every
evaluation, and an error hook sees every failure.
Fibers and threads¶
- Everything except the provider call runs on the calling fiber: context merge, hooks, the transaction lookup and the status gate.
- The provider call runs on ZIO's blocking pool, because providers are blocking Java code. SDK-level hooks (added with
addApiHook) and the provider's own hooks run there too, inside the call. - Provider events arrive on the SDK's threads.
FeatureFlagsbridges each one into ZIO, updates the provider status, then publishes it on a sliding hub of 256 events, so a slow subscriber loses the oldest events, never the newest.
State¶
| State | Held in | Scope |
|---|---|---|
| Global and client context | Ref |
the FeatureFlags instance |
Scoped context (withContext) |
FiberRef |
the fiber and its children |
| Transaction (overrides, cache, recorded flags) | FiberRef |
the transaction block |
| Hooks | Ref |
the instance; API-level ZIO hooks are shared by a registry's clients |
| Provider status | a serialized state machine | the instance |
Because scoped context and transactions are fiber-local, concurrent requests never see each other's targeting or overrides.
Sharing the SDK¶
fromProvider and its variants register the provider with the SDK's global OpenFeatureAPI. Without a domain, the
FeatureFlags owns that API and shuts it down when its scope closes; with a domain, it shares the API and leaves it
running. FeatureFlagRegistry instead creates an isolated OpenFeatureAPI of its own, with one client per domain, so
it cannot interfere with anything else in the JVM. See Wiring a provider.
Modules¶
graph LR
core["zio-openfeature-core"] --> sdk["OpenFeature Java SDK"]
extras["extras: HOCON, env vars, caching, circuit breaker"] --> core
ofrep["ofrep"] --> core
optimizely["optimizely"] --> core
testkit["testkit"] --> core
ofrep --> ofrepj["OFREP contrib provider"]
optimizely --> opt["Optimizely Java SDK"]
Only core is required. The others are separate artifacts, so you pull in an HTTP stack or the Optimizely SDK only
if you use them. Every module is cross-built for Scala 2.13 and 3; a few conveniences, such as derives FlagType, are
Scala 3 only.