Getting Started

Table of contents

  1. Installation
  2. Basic Usage
    1. Setting Up with a Provider
    2. Setting Up for Testing
  3. Evaluating Flags
    1. Boolean Flags
    2. String Flags
    3. Numeric Flags
    4. Object Flags
    5. Detailed Evaluation
    6. Total Evaluation (never fails)
  4. Using Evaluation Context
    1. Setting Global Context
    2. Scoped Context
  5. Factory Methods
    1. From Any OpenFeature Provider
    2. With Domain Isolation, Hooks, Timeouts, …
    3. With Multiple Providers
  6. Tracking Events
  7. Event Handlers
  8. Next Steps

Installation

Maven Central

Add the following to your build.sbt, replacing <version> with the version shown in the badge above:

// Core library (includes OpenFeature SDK)
libraryDependencies += "io.github.etacassiopeia" %% "zio-openfeature-core" % "<version>"

// Built-in providers: HOCON, env vars, caching wrapper (optional)
libraryDependencies += "io.github.etacassiopeia" %% "zio-openfeature-extras" % "<version>"

// OFREP — OpenFeature Remote Evaluation Protocol provider (HTTP)
libraryDependencies += "io.github.etacassiopeia" %% "zio-openfeature-ofrep" % "<version>"

// Optimizely Feature Experimentation — direct integration on top of the Optimizely Java SDK
libraryDependencies += "io.github.etacassiopeia" %% "zio-openfeature-optimizely" % "<version>"

// For testing
libraryDependencies += "io.github.etacassiopeia" %% "zio-openfeature-testkit" % "<version>" % Test

You’ll also need an OpenFeature provider for your feature flag service:

// Example: flagd provider
libraryDependencies += "dev.openfeature.contrib.providers" % "flagd" % "0.8.9"

// Or: LaunchDarkly
libraryDependencies += "dev.openfeature.contrib.providers" % "launchdarkly" % "1.1.0"

See the OpenFeature ecosystem for all available providers.

Basic Usage

Setting Up with a Provider

import zio.*
import zio.openfeature.*
import dev.openfeature.contrib.providers.flagd.FlagdProvider

object MyApp extends ZIOAppDefault:

  val program = for
    enabled <- FeatureFlags.boolean("my-feature", default = false)
    _       <- ZIO.when(enabled)(Console.printLine("Feature enabled!"))
  yield ()

  def run = program.provide(
    Scope.default >>> FeatureFlags.fromProvider(new FlagdProvider())
  )

Setting Up for Testing

import zio.*
import zio.openfeature.*
import zio.openfeature.testkit.*

object TestApp extends ZIOAppDefault:

  val program = for
    enabled <- FeatureFlags.boolean("my-feature", default = false)
    _       <- ZIO.when(enabled)(Console.printLine("Feature enabled!"))
  yield ()

  def run = program.provide(
    TestFeatureProvider.scopedLayer(Map("my-feature" -> true))
  )

Evaluating Flags

Boolean Flags

val enabled: ZIO[FeatureFlags, FeatureFlagError, Boolean] =
  FeatureFlags.boolean("feature-toggle", default = false)

String Flags

val variant: ZIO[FeatureFlags, FeatureFlagError, String] =
  FeatureFlags.string("button-color", default = "blue")

Numeric Flags

val limit: ZIO[FeatureFlags, FeatureFlagError, Int] =
  FeatureFlags.int("max-items", default = 100)

val rate: ZIO[FeatureFlags, FeatureFlagError, Double] =
  FeatureFlags.double("sample-rate", default = 0.1)

val count: ZIO[FeatureFlags, FeatureFlagError, Long] =
  FeatureFlags.long("max-bytes", default = 1000000L)

Object Flags

val config: ZIO[FeatureFlags, FeatureFlagError, Map[String, Any]] =
  FeatureFlags.obj("feature-config", default = Map("timeout" -> 30))

Detailed Evaluation

Get full resolution details including variant, reason, and metadata:

val details: ZIO[FeatureFlags, FeatureFlagError, FlagResolution[Boolean]] =
  FeatureFlags.booleanDetails("feature", default = false)

details.map { resolution =>
  println(s"Value: ${resolution.value}")
  println(s"Variant: ${resolution.variant}")
  println(s"Reason: ${resolution.reason}")
  println(s"Flag Key: ${resolution.flagKey}")
}

Total Evaluation (never fails)

The methods above surface evaluation errors in a typed error channel. When you would rather always get a value — falling back to the default on any error, per the OpenFeature spec’s “total” evaluation (§1.4.10) — use the *OrDefault variants. They return UIO, so there is no error to handle:

val enabled: ZIO[FeatureFlags, Nothing, Boolean] =
  FeatureFlags.booleanOrDefault("feature-toggle", default = false)

resolveOrDefault is the total form of booleanDetails/valueDetails: it never fails, but the returned FlagResolution still tells you why the default was served — reason is Error and errorCode/errorMessage are populated when a fallback occurred.

FeatureFlags.resolveOrDefault[Boolean]("feature-toggle", default = false).map { resolution =>
  if (resolution.errorCode.isDefined) println(s"Served default: ${resolution.errorMessage}")
}

Both typed errors and unexpected defects are absorbed into the default; only fiber interruption still propagates.

Using Evaluation Context

Pass user and environment information for targeted flag evaluation:

// Create context with targeting key (user ID)
val ctx = EvaluationContext("user-123")
  .withAttribute("plan", "premium")
  .withAttribute("country", "US")
  .withAttribute("beta", true)

// Evaluate with context
FeatureFlags.boolean("premium-feature", default = false, ctx)

Setting Global Context

Apply context to all evaluations:

val globalCtx = EvaluationContext.empty
  .withAttribute("app_version", "2.0.0")
  .withAttribute("environment", "production")

FeatureFlags.setGlobalContext(globalCtx)

Scoped Context

Apply context to a block of code:

val requestCtx = EvaluationContext("user-456")
  .withAttribute("session_id", sessionId)

FeatureFlags.withContext(requestCtx) {
  // All evaluations in this block use requestCtx
  for
    a <- FeatureFlags.boolean("feature-a", default = false)
    b <- FeatureFlags.string("feature-b", default = "control")
  yield (a, b)
}

Factory Methods

From Any OpenFeature Provider

import dev.openfeature.sdk.FeatureProvider

val provider: FeatureProvider = // any OpenFeature provider

val layer = FeatureFlags.fromProvider(provider)

With Domain Isolation, Hooks, Timeouts, …

Anything beyond the plain provider — a named domain, initial hooks, custom timeouts, async init, or any combination — goes through FeatureFlagsConfig and the config-driven factory FeatureFlags.fromProvider(provider, config):

// Domain isolation, for multi-provider setups
val layer = FeatureFlags.fromProvider(provider, FeatureFlagsConfig().withDomain("my-domain"))

// Optionally include a version for telemetry/debugging
val versionedLayer = FeatureFlags.fromProvider(provider, FeatureFlagsConfig().withDomain("my-domain").withVersion("1.0.0"))

// Initial hooks
val hooks = List(
  FeatureHook.logging(),
  FeatureHook.metrics((k, d, s) => ZIO.unit)
)
val hookedLayer = FeatureFlags.fromProvider(provider, FeatureFlagsConfig().withHooks(hooks))

// Domain + hooks together — not expressible with the pre-#253 factory overloads
val combinedLayer = FeatureFlags.fromProvider(provider, FeatureFlagsConfig().withDomain("my-domain").withHooks(hooks))

With Multiple Providers

Combine multiple providers using the SDK’s MultiProvider support via FeatureFlags.multiProvider, then pass the result into fromProvider like any other provider:

val layer = FeatureFlags.fromProvider(FeatureFlags.multiProvider(List(localProvider, remoteProvider)), FeatureFlagsConfig())

See Factory Methods in the Providers guide for the full FeatureFlagsConfig reference, including InitMode (sync vs async) and ApiOwnership.

Tracking Events

Track user actions for analytics and experimentation:

// Simple event tracking
FeatureFlags.track("button-clicked")

// Track with user context
FeatureFlags.track("purchase", EvaluationContext("user-123"))

// Track with event details
val details = TrackingEventDetails(
  value = Some(99.99),
  attributes = Map("currency" -> "USD", "items" -> 3)
)
FeatureFlags.track("checkout", details)

Event Handlers

React to provider lifecycle events:

// Handle provider ready
FeatureFlags.onProviderReady { metadata =>
  ZIO.logInfo(s"Provider ${metadata.name} is ready")
}

// Handle configuration changes
FeatureFlags.onConfigurationChanged { (flags, metadata) =>
  ZIO.logInfo(s"Flags changed: ${flags.mkString(", ")}")
}

// Handle errors
FeatureFlags.onProviderError { (error, metadata) =>
  ZIO.logError(s"Provider error: ${error.getMessage}")
}

Next Steps


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

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