Providers

Table of contents

  1. Overview
  2. Using Optimizely
    1. Installation
    2. Getting Your SDK Key
    3. Basic Setup
    4. User Targeting
    5. Feature Variables
    6. Complete Example
  3. Other Providers
  4. Using flagd
    1. Installation
    2. Setup
  5. Using LaunchDarkly
    1. Installation
    2. Setup
  6. Factory Methods
    1. fromProvider
    2. fromProviderWithDomain
    3. fromProviderWithHooks
    4. fromMultiProvider
  7. Provider Lifecycle
    1. Initialization
    2. Shutdown
    3. Provider Status
    4. Provider Metadata
  8. Provider Events
    1. Event Handlers
    2. Event Stream
  9. Testing
  10. Best Practices
    1. 1. Use Environment Variables for Credentials
    2. 2. Initialize Early
    3. 3. Handle Errors Gracefully
    4. 4. Provide User Context
    5. 5. Use Domain Isolation in Tests

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

  1. Log in to Optimizely
  2. Navigate to SettingsEnvironments
  3. 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()}"
)

Copyright © 2026 Mohsen Zainalpour. Distributed under the Apache 2.0 license.

This site uses Just the Docs, a documentation theme for Jekyll.