ZIO OpenFeature
A ZIO-native wrapper around the OpenFeature Java SDK for Scala 2.13 and Scala 3.
Why ZIO OpenFeature?
ZIO OpenFeature wraps the OpenFeature Java SDK, giving you access to the entire OpenFeature ecosystem while providing a type-safe, functional API designed for ZIO applications.
- Use Any Provider - Works with all OpenFeature providers: LaunchDarkly, Flagsmith, Flipt, flagd, and more
- Type Safety - Compile-time guarantees with the
FlagTypetype class - ZIO Native - Effect-based API with proper resource management
- Transactions - Scoped flag overrides with caching and evaluation tracking
- Testkit - In-memory provider for testing without external dependencies
Quick Example
import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.flagd.FlagdProvider
object MyApp extends ZIOAppDefault:
val program = for
enabled <- FeatureFlags.boolean("new-feature", default = false)
_ <- ZIO.when(enabled)(Console.printLine("Feature enabled!"))
yield ()
def run = program.provide(
Scope.default >>> FeatureFlags.fromProvider(new FlagdProvider())
)
Two ways to read a flag
The call above names the key, the type and the default at the use site. That is the direct style, and it is fully supported. If the same flag is read from more than one place, declaring it once is usually the simpler option — the key, type and default stop being repeated, and a second call site can no longer disagree about them:
object Flags:
val NewFeature = FlagDef("new-feature", false, "the new checkout flow")
// anywhere
FeatureFlags.value(Flags.NewFeature) // ZIO[FeatureFlags, FeatureFlagError, Boolean]
FeatureFlags.valueOrDefault(Flags.NewFeature) // never fails — serves the definition's default
Both styles run through the same evaluation path — hooks, transactions, caching and error semantics are identical, and you can mix them in one codebase. See Typed Flags.
Documentation
Start with Getting Started, then Typed Flags if you want the declare-once style. The rest are reference pages — read them when you reach the problem they solve.
| Section | Description |
|---|---|
| Getting Started | Installation and basic usage |
| Typed Flags | Declare a flag once — key, type and default — and evaluate it by name |
| Architecture | Core design and components |
| Providers | Using OpenFeature providers |
| Extras | HOCON, env var, and caching providers |
| Optimizely | Optimizely Feature Experimentation integration |
| Evaluation Context | Targeting and context hierarchy |
| Hooks | Cross-cutting concerns |
| Transactions | Flag overrides and tracking |
| Testkit | Testing utilities |
| Testing Real Providers | Fault-testing a real provider over TLS-MITM |
| Spec Compliance | OpenFeature specification compliance |
Added in 1.1.0
Everything below is documented in the pages above; this table is a shortcut to the right section.
| Feature | What it gives you | Where |
|---|---|---|
| Typed tier fails on error codes | value / *Details fail with a typed FlagNotFound, TypeMismatch, … when the provider reports an error code — a fail-closed gate stays closed; the total tier still serves the default | Getting Started |
NestedPolicy.Reuse | An inner transaction runs inside the enclosing one instead of failing — a per-request transaction is safe as middleware | Transactions |
transactionEvaluations | None outside a transaction, Some(...) inside — an audit read can no longer mistake “no transaction” for “nothing evaluated” | Transactions |
Wrappers take any FeatureProvider | CircuitBreakerProvider and CachingProvider no longer require an EventProvider | Extras |
FlagDef[A] | Declare a flag once and evaluate it by name | Typed Flags |
derives FlagType | Codecs for your own enums and case classes, no boilerplate | Typed Flags |
FlagType.wireType | A domain type carried over the wire as a scalar resolves through that scalar’s method, and hooks see it | Architecture |
Native 64-bit Long | long/longDetails resolve the full Long range exactly, instead of losing precision past 2^53 | Extras |
ContextSource | Pull ambient context (MDC, tracing, correlation id) into every evaluation | Evaluation Context |
FallbackLogging | A served default leaves a warn line, rate-limited per flag key | Getting Started |
verify + AcquireStatus | Reject a real provider that constructed but cannot serve, and ask whether it is live yet | Providers |
FLAG_NOT_FOUND for absent keys | A provider that does not hold a key lets a MultiProvider chain advance | Extras, Testkit |
Typed fixtures (:=) | Pin a test fixture through the flag’s own codec | Testkit |
TestFeatureProvider.makeNamed | Distinct metadata names, so a chain of test providers is really a chain | Testkit |
Modules
| Module | Description |
|---|---|
| core | ZIO wrapper around OpenFeature SDK |
| extras | Built-in HOCON, env var, and caching providers |
| ofrep | OpenFeature Remote Evaluation Protocol (OFREP) provider over HTTP |
| optimizely | First-party Optimizely Feature Experimentation integration |
| testkit | In-memory provider for testing |
Requirements
- Scala 2.13+ or Scala 3.3+
- ZIO 2.1+
License
ZIO OpenFeature is distributed under the Apache 2.0 License.