Providers
Table of contents
- Overview
- Using Optimizely
- Other Providers
- Using flagd
- Using LaunchDarkly
- Factory Methods
- Provider Lifecycle
- Provider Events
- Testing
- Best Practices
Overview
ZIO OpenFeature wraps the OpenFeature Java SDK, giving you access to the entire ecosystem of OpenFeature providers. Use any provider that implements the OpenFeature specification.
import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.optimizely.OptimizelyProvider
val layer = FeatureFlags.fromProvider(provider)
program.provide(Scope.default >>> layer)
Using Optimizely
Optimizely is the recommended provider for most users. It provides feature flags, A/B testing, and experimentation capabilities.
Installation
libraryDependencies ++= Seq(
"io.github.etacassiopeia" %% "zio-openfeature-core" % "<version>",
"dev.openfeature.contrib.providers" % "optimizely" % "0.1.0"
)
Getting Your SDK Key
- Log in to Optimizely
- Navigate to Settings → Environments
- Copy the SDK Key for your environment (Development, Staging, or Production)
Basic Setup
import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.optimizely.OptimizelyProvider
import dev.openfeature.contrib.providers.optimizely.OptimizelyProviderConfig
object MyApp extends ZIOAppDefault:
val config = OptimizelyProviderConfig.builder()
.sdkKey("YOUR_SDK_KEY")
.build()
val provider = new OptimizelyProvider(config)
val program = for
enabled <- FeatureFlags.boolean("new-checkout-flow", default = false)
_ <- ZIO.when(enabled)(Console.printLine("New checkout enabled!"))
yield ()
def run = program.provide(
Scope.default >>> FeatureFlags.fromProvider(provider)
)
User Targeting
The targetingKey in the evaluation context maps to the Optimizely user ID:
// Create context with user ID
val userContext = EvaluationContext("user-12345")
.withAttribute("email", "user@example.com")
.withAttribute("plan", "premium")
.withAttribute("country", "US")
// Evaluate with user context
val result = FeatureFlags.boolean("premium-feature", false, userContext)
Feature Variables
Optimizely supports feature variables. By default, the OpenFeature provider searches for a variable named "value":
// Optimizely feature with a "value" variable
val discount = FeatureFlags.double("discount-rate", 0.0, userContext)
To use a different variable name, add the variableKey attribute to the context:
val ctx = userContext.withAttribute("variableKey", "custom_variable_name")
val value = FeatureFlags.string("feature-key", "default", ctx)
Complete Example
import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.optimizely.OptimizelyProvider
import dev.openfeature.contrib.providers.optimizely.OptimizelyProviderConfig
object FeatureFlagApp extends ZIOAppDefault:
val sdkKey = sys.env.getOrElse("OPTIMIZELY_SDK_KEY", "YOUR_SDK_KEY")
val config = OptimizelyProviderConfig.builder()
.sdkKey(sdkKey)
.build()
val provider = new OptimizelyProvider(config)
def handleUserRequest(userId: String, plan: String) = for
ctx = EvaluationContext(userId).withAttribute("plan", plan)
// Check if new checkout is enabled for this user
newCheckout <- FeatureFlags.boolean("new-checkout-flow", false, ctx)
// Get the button variation
buttonColor <- FeatureFlags.string("checkout-button", "blue", ctx)
// Get max cart items
maxItems <- FeatureFlags.int("max-cart-items", 10, ctx)
_ <- Console.printLine(s"User $userId (plan: $plan):")
_ <- Console.printLine(s" - New checkout: $newCheckout")
_ <- Console.printLine(s" - Button color: $buttonColor")
_ <- Console.printLine(s" - Max items: $maxItems")
yield ()
val program = for
_ <- handleUserRequest("user-001", "free")
_ <- handleUserRequest("user-002", "premium")
yield ()
def run = program.provide(
Scope.default >>> FeatureFlags.fromProvider(provider)
)
Other Providers
The OpenFeature ecosystem includes providers for many feature flag services:
| Provider | Dependency | Description |
|---|---|---|
| Optimizely | "dev.openfeature.contrib.providers" % "optimizely" % "0.1.0" | Feature flags and A/B testing |
| flagd | "dev.openfeature.contrib.providers" % "flagd" % "0.8.9" | Open-source flag evaluation engine |
| LaunchDarkly | "dev.openfeature.contrib.providers" % "launchdarkly" % "1.1.0" | Enterprise feature management |
| Flagsmith | "dev.openfeature.contrib.providers" % "flagsmith" % "0.1.0" | Open-source feature flags |
| Flipt | "dev.openfeature.contrib.providers" % "flipt" % "0.2.0" | Open-source feature flags |
| Statsig | "dev.openfeature.contrib.providers" % "statsig" % "0.2.1" | Feature gates and experiments |
| Unleash | "dev.openfeature.contrib.providers" % "unleash" % "0.1.3" | Open-source feature management |
| ConfigCat | "dev.openfeature.contrib.providers" % "configcat" % "0.1.0" | Feature flags for teams |
| Go Feature Flag | "dev.openfeature.contrib.providers" % "go-feature-flag" % "0.3.0" | Simple feature flag solution |
Check Maven Central for the latest versions.
Using flagd
flagd is an open-source, cloud-native feature flag evaluation engine.
Installation
libraryDependencies ++= Seq(
"io.github.etacassiopeia" %% "zio-openfeature-core" % "<version>",
"dev.openfeature.contrib.providers" % "flagd" % "0.8.9"
)
Setup
import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.flagd.FlagdProvider
import dev.openfeature.contrib.providers.flagd.FlagdOptions
object MyApp extends ZIOAppDefault:
// Default: connects to localhost:8013
val provider = new FlagdProvider()
// Or with custom options
val customProvider = new FlagdProvider(
FlagdOptions.builder()
.host("flagd.example.com")
.port(8013)
.tls(true)
.build()
)
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(provider)
)
Using LaunchDarkly
LaunchDarkly is an enterprise feature management platform.
Installation
libraryDependencies ++= Seq(
"io.github.etacassiopeia" %% "zio-openfeature-core" % "<version>",
"dev.openfeature.contrib.providers" % "launchdarkly" % "1.1.0"
)
Setup
import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.launchdarkly.LaunchDarklyProvider
import dev.openfeature.contrib.providers.launchdarkly.LaunchDarklyProviderOptions
object MyApp extends ZIOAppDefault:
val sdkKey = sys.env.getOrElse("LAUNCHDARKLY_SDK_KEY", "your-sdk-key")
val options = LaunchDarklyProviderOptions.builder()
.sdkKey(sdkKey)
.build()
val provider = new LaunchDarklyProvider(options)
def run = program.provide(
Scope.default >>> FeatureFlags.fromProvider(provider)
)
Factory Methods
ZIO OpenFeature provides several factory methods to create the FeatureFlags layer:
fromProvider
Create from any OpenFeature provider:
import dev.openfeature.sdk.FeatureProvider
val provider: FeatureProvider = new OptimizelyProvider(config)
val layer: ZLayer[Scope, Throwable, FeatureFlags] =
FeatureFlags.fromProvider(provider)
fromProviderWithDomain
Create with a named domain for test isolation. Each domain gets its own client:
val layer = FeatureFlags.fromProviderWithDomain(provider, "my-service")
// Useful for testing - each test can use a different domain
val testLayer = FeatureFlags.fromProviderWithDomain(testProvider, "test-domain-123")
fromProviderWithHooks
Create with initial hooks:
val hooks = List(
FeatureHook.logging(),
FeatureHook.metrics((k, d, s) => ZIO.unit)
)
val layer = FeatureFlags.fromProviderWithHooks(provider, hooks)
fromMultiProvider
Create from multiple providers using the SDK’s MultiProvider support. The first provider that returns without error is used:
import dev.openfeature.sdk.FeatureProvider
val localProvider: FeatureProvider = // local overrides
val remoteProvider: FeatureProvider = // remote service
// Uses first-match strategy by default
val layer = FeatureFlags.fromMultiProvider(List(localProvider, remoteProvider))
You can also supply a custom strategy:
import dev.openfeature.sdk.multiprovider.FirstSuccessfulStrategy
val layer = FeatureFlags.fromMultiProvider(
List(primaryProvider, fallbackProvider),
new FirstSuccessfulStrategy()
)
Provider Lifecycle
Initialization
When you create a FeatureFlags layer, the provider is automatically initialized using setProviderAndWait. This ensures the provider is ready before any flag evaluations:
// Provider is initialized when layer is provided
program.provide(Scope.default >>> FeatureFlags.fromProvider(provider))
Shutdown
When the scope ends, the OpenFeature API is automatically shut down. Shutdown resets the provider status to NotReady, clears hooks and contexts, shuts down the event hub (terminating all event stream subscribers), and calls the SDK’s shutdown:
ZIO.scoped {
for
ff <- ZIO.service[FeatureFlags]
// Use feature flags
yield ()
}
// Provider shutdown automatically on scope exit
// Explicit shutdown is also available
FeatureFlags.shutdown
Provider Status
Check if the provider is ready:
for
status <- FeatureFlags.providerStatus
_ <- ZIO.when(status == ProviderStatus.Ready) {
Console.printLine("Provider is ready")
}
yield ()
Provider Metadata
Get information about the current provider:
for
metadata <- FeatureFlags.providerMetadata
_ <- Console.printLine(s"Provider: ${metadata.name}")
yield ()
Provider Events
Java SDK provider events are automatically bridged to the ZIO event system. When a provider emits events (Ready, Error, Stale, ConfigurationChanged), they are captured via Java SDK event listeners and published to the ZIO event hub. The provider status is also kept in sync automatically. Event bridge handlers are registered during layer creation and cleaned up when the scope ends.
ZIO OpenFeature provides two ways to consume provider events: event handlers and event streams.
Event Handlers
Register handlers for specific event types. Handlers return a cancellation effect per OpenFeature spec 5.2.7:
// Handler for ready events
val cancelReady = FeatureFlags.onProviderReady { metadata =>
ZIO.logInfo(s"Provider ${metadata.name} is ready")
}
// Handler for error events
val cancelError = FeatureFlags.onProviderError { (error, metadata) =>
ZIO.logError(s"Provider ${metadata.name} error: ${error.getMessage}")
}
// Handler for stale events
val cancelStale = FeatureFlags.onProviderStale { (reason, metadata) =>
ZIO.logWarning(s"Provider ${metadata.name} stale: $reason")
}
// Handler for configuration changed events
val cancelConfig = FeatureFlags.onConfigurationChanged { (flags, metadata) =>
ZIO.logInfo(s"Flags changed: ${flags.mkString(", ")}")
}
// Generic handler for any event type
val cancelGeneric = FeatureFlags.on(ProviderEventType.Ready, event =>
ZIO.logInfo(s"Event: $event")
)
// Cancel handler when no longer needed
cancelReady.flatMap(cancel => cancel)
Per OpenFeature spec 5.3.3, handlers run immediately if the provider is already in the matching state:
// If provider is already ready, this handler runs immediately
FeatureFlags.onProviderReady { metadata =>
ZIO.logInfo("Provider ready!") // Runs right away if already ready
}
Event Stream
For reactive event processing, use the ZStream:
val eventHandler = FeatureFlags.events.foreach { event =>
event match
case ProviderEvent.Ready(meta) =>
ZIO.logInfo(s"Provider ${meta.name} is ready")
case ProviderEvent.ConfigurationChanged(flags, meta) =>
ZIO.logInfo(s"Flags changed: ${flags.mkString(", ")}")
case ProviderEvent.Stale(reason, meta) =>
ZIO.logWarning(s"Provider data stale: $reason")
case ProviderEvent.Error(error, meta, errorCode, errorMessage) =>
ZIO.logError(s"Provider error: ${errorMessage.getOrElse(error.getMessage)}")
case ProviderEvent.Reconnecting(meta) =>
ZIO.logInfo(s"Provider ${meta.name} reconnecting...")
}
// Run event handler in background
eventHandler.fork
Testing
For testing, use TestFeatureProvider from the testkit module:
import zio.openfeature.testkit.*
val testLayer = TestFeatureProvider.layer(Map(
"feature-a" -> true,
"feature-b" -> "variant-1",
"max-items" -> 50
))
val testProgram = for
a <- FeatureFlags.boolean("feature-a", false)
b <- FeatureFlags.string("feature-b", "control")
n <- FeatureFlags.int("max-items", 10)
yield (a, b, n) // (true, "variant-1", 50)
testProgram.provide(Scope.default >>> testLayer)
See Testkit for more testing utilities.
Best Practices
1. Use Environment Variables for Credentials
val sdkKey = sys.env.getOrElse("OPTIMIZELY_SDK_KEY", "fallback-key")
2. Initialize Early
Create the FeatureFlags layer at application startup to ensure providers are ready before flag evaluations.
3. Handle Errors Gracefully
FeatureFlags.boolean("feature", false)
.catchAll {
case FeatureFlagError.ProviderNotReady =>
ZIO.succeed(false) // Safe default
case FeatureFlagError.ProviderError(cause) =>
ZIO.logError(s"Provider error: $cause") *> ZIO.succeed(false)
case _ =>
ZIO.succeed(false)
}
4. Provide User Context
Always include a targeting key for consistent user experiences:
val ctx = EvaluationContext(userId)
.withAttribute("plan", userPlan)
FeatureFlags.boolean("feature", false, ctx)
5. Use Domain Isolation in Tests
val testLayer = FeatureFlags.fromProviderWithDomain(
testProvider,
s"test-${java.util.UUID.randomUUID()}"
)