Skip to content

A pure java implementation of the LaunchDarkly client/mobile APIs

License

Notifications You must be signed in to change notification settings

block/java-client-for-ld

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

JVM Client SDK for LaunchDarkly

A lightweight JVM Client SDK for LaunchDarkly platforms that supports both mobile keys and client-side IDs. This SDK is designed for scenarios where you need to evaluate LaunchDarkly feature flags using client-side SDK endpoints. Both key types are not considered sensitive and may be committed to repos or distributed with applications.

Important

This is not an official SDK. You may want to review the list of official SDKs to determine if one of those suits your needs first.

Features

  • Dual Key Support: Automatically detects and uses either mobile keys (mob-*) or client-side IDs
  • Real-time Streaming: Server-Sent Events (SSE) support for instant flag updates
  • Simple API: Easy-to-use interface for feature flag evaluation
  • Type-Safe Values: Strong typing for boolean, string, numeric, and JSON flag values
  • Context Support: Full support for LaunchDarkly's context model with custom attributes
  • Flag Change Listeners: Subscribe to flag updates in real-time
  • Gradle Integration: Use feature flags during Gradle configuration phase

Modules

This repository contains two modules:

Installation

Runtime SDK

Add the SDK to your build.gradle(.kts) file:

dependencies {
  implementation("xyz.block.launchdarkly:ld-java-client:0.1.0")
}

Quick Start

Basic Usage

// Create a context (represents a user)
val context = LDContext.create("user-123")  // Simple creation
// Or with details:
val detailedContext = LDContext.builder("user-123")
  .name("Jane Doe")
  .email("jane@example.com")
  .build()

// Initialize the client with your SDK key
val client = LDClient.create(
  sdkKey = "your-key",  // or your client-side ID or mobile sdk key
  context = context
)

// Option 1: Chaining API (like official SDK) - throws if missing/wrong type
val isEnabled = client.allFlagsState()
  .getFlagValue("new-feature")
  .booleanValue()

// Option 2: Direct variation methods with defaults
val welcomeMessage = client.stringVariation("welcome-message", defaultValue = "Hello!")
val maxRetries = client.numberVariation("max-retries", defaultValue = 3.0)

// Option 3: Strict methods that throw on missing flags
val configValue = client.stringVariationOrThrow("required-config")

// Close the client when done
client.close()

Configuration Options

val config = LDConfig.builder()
  .connectionTimeout(10.seconds)
  .readTimeout(30.seconds)
  .streamingEnabled(true) // Enable real-time streaming (default: true)
  .logLevel(LDLogLevel.DEBUG) // Set log level
  .logger { level, message, throwable ->
    // Custom logger implementation
    when (level) {
      LDLogLevel.ERROR -> println("ERROR: $message", throwable)
      LDLogLevel.WARN -> println("WARN: $message")
      LDLogLevel.INFO -> println("INFO: $message")
      LDLogLevel.DEBUG -> println("DEBUG: $message")
    }
  }
  .build()

val client = LDClient.create(
  sdkKey = "your-key",  // or your client-side ID or mobile sdk key
  context = context,
  config = config
)

The .logger() method accepts an LDLoggerFunction - a functional interface that makes it easy to integrate with any logging framework. Thanks to Kotlin's SAM conversion, you can pass a lambda directly.

Real-time Flag Updates

The SDK supports real-time flag updates via Server-Sent Events (SSE):

val client = LDClient.create(
  sdkKey = "your-key",  // or your client-side ID or mobile sdk key
  context = context
)

// Listen for flag changes
client.addFlagChangeListener { event ->
  println("Flag ${event.key} changed to: ${event.value}")
}

// Your application continues to receive updates automatically
// No need to poll!

// Remove listener when done
// client.removeFlagChangeListener(listener)

Disable Streaming (Polling Mode)

If you prefer polling to streaming:

val config = LDConfig.builder()
  .streamingEnabled(false)  // Disable streaming
  .build()

val client = LDClient.create(
  sdkKey = "your-key",  // or your client-side ID or mobile sdk key
  context = context,
  config = config
)

// Manually refresh flags when needed
client.refresh()

Working with Flag Values

// Check if a flag exists
if (client.isFlagKnown("feature-x")) {
  val value = client.getFlag("feature-x")

  // Type-safe access
  when {
    value.isBoolean() -> println("Boolean: ${value.asBooleanOrNull()}")
    value.isString() -> println("String: ${value.asStringOrNull()}")
    value.isNumber() -> println("Number: ${value.asDoubleOrNull()}")
    value.isObject() -> println("Object: ${value.asJsonObjectOrNull()}")
    value.isArray() -> println("Array: ${value.asJsonArrayOrNull()}")
  }
}

// Get all flags at once
val allFlags = client.getAllFlags()
allFlags.forEach { (key, value) ->
  println("$key = ${value.toJsonString()}")
}

// Refresh flags from LaunchDarkly
client.refresh()

Advanced Context Configuration

val context = LDContext.builder("user-456")
  .name("John Smith")
  .email("john@company.com")
  .kind(ContextKind.DEFAULT) // Or .kind("user") - Default is "user"
  .custom("team", "engineering")
  .custom("role", "developer")
  .custom(mapOf(
    "department" to "R&D",
    "location" to "SF"
  ))
  .build()

// Predefined context kinds (enum constants)
val orgContext = LDContext.builder("org-123")
  .kind(ContextKind.ORGANIZATION) // Enum constant
  .name("Acme Corp")
  .build()

val deviceContext = LDContext.builder("device-456")
  .kind(ContextKind.DEVICE) // Or .kind("device") - String overload also available
  .custom("platform", "android")
  .build()

// Custom context kinds (use string)
val customContext = LDContext.builder("custom-123")
  .kind("my-custom-kind")
  .build()

Multi-Context Support

LaunchDarkly supports evaluating flags based on multiple context kinds simultaneously:

// Create individual contexts
val userContext = LDContext.builder("user-123")
  .name("Alice")
  .email("alice@company.com")
  .build()

val orgContext = LDContext.builder("org-456")
  .kind("organization")
  .name("Acme Corp")
  .custom("tier", "enterprise")
  .build()

val deviceContext = LDContext.builder("device-789")
  .kind("device")
  .custom("platform", "android")
  .build()

// Combine into a multi-context
val multiContext = LDContext.createMulti(userContext, orgContext, deviceContext)

// Use it with the client
val client = LDClient.create(sdkKey = "mob-your-key", context = multiContext)

// Flags can now target based on any of these contexts
val isFeatureEnabled = client.boolVariation("new-feature")

Differences from Official SDKs

vs. LaunchDarkly Java Server SDK

Feature This SDK Official Server SDK
API Key Type Mobile (mob-*) or Client-side ID Server (sdk-*)
Endpoints Client-side SDK endpoints Server SDK endpoints
Streaming ✅ Yes (SSE) ✅ Yes (SSE)
Events ❌ No ✅ Yes

vs. LaunchDarkly Android/iOS SDKs

This SDK provides similar functionality to the mobile SDKs but runs on any JVM platform (server, CLI tools, desktop apps, etc.), not just Android.

Use Cases

This SDK is perfect for:

  • CLI Tools: Feature flags in command-line applications
  • Background Services: Daemons and background jobs
  • Build Tools: Gradle plugins and build automation
  • Desktop Applications: JVM desktop apps that need feature flags

Examples

CLI Tool Example

fun main() {
  val context = LDContext.builder(System.getProperty("user.name"))
    .custom("hostname", InetAddress.getLocalHost().hostName)
    .build()

  val client = LDClient.create(
    sdkKey = System.getenv("LAUNCH_DARKLY_SDK_KEY"),
    context = context
  )

  if (client.boolVariation("enable-experimental-features")) {
    runExperimentalFeatures()
  }

  client.close()
}

Gradle Plugin Example

class MyPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    val context = LDContext.builder(project.name)
      .custom("gradle-version", project.gradle.gradleVersion)
      .build()

    val client = LDClient.create(
      sdkKey = "mob-your-key",  // or your client-side ID
      context = context
    )

    if (client.boolVariation("enable-new-task")) {
      project.tasks.register("newTask") {
        // Task implementation
      }
    }

    client.close()
  }
}

Custom Logger Integration

You can integrate with any logging framework by implementing the LDLoggerFunction interface:

// Lambda style (recommended for simple cases)
val config = LDConfig.builder()
  .logLevel(LDLogLevel.INFO)
  .logger { level, message, throwable ->
    val slf4jLogger = LoggerFactory.getLogger("LaunchDarkly")
    when (level) {
      LDLogLevel.ERROR -> slf4jLogger.error(message, throwable)
      LDLogLevel.WARN -> slf4jLogger.warn(message, throwable)
      LDLogLevel.INFO -> slf4jLogger.info(message)
      LDLogLevel.DEBUG -> slf4jLogger.debug(message)
    }
  }
  .build()

// Or implement the interface directly for reusable loggers
class Slf4jLDLogger(private val name: String) : LDLoggerFunction {
  private val logger = LoggerFactory.getLogger(name)

  override fun log(level: LDLogLevel, message: String, throwable: Throwable?) {
    when (level) {
      LDLogLevel.ERROR -> logger.error(message, throwable)
      LDLogLevel.WARN -> logger.warn(message, throwable)
      LDLogLevel.INFO -> logger.info(message)
      LDLogLevel.DEBUG -> logger.debug(message)
    }
  }
}

val configWithCustomLogger = LDConfig.builder()
  .logger(Slf4jLDLogger("LaunchDarkly"))
  .build()

Daemon Service Example with Real-time Updates

class FeatureFlagService(sdkKey: String) {
  private val client: LDClient
  private val logger = LoggerFactory.getLogger(FeatureFlagService::class.java)

  init {
    val context = LDContext.builder("daemon-service")
      .custom("version", BuildConfig.VERSION)
      .build()

    client = LDClient.create(sdkKey, context)

    // Listen for flag changes and react in real-time
    client.addFlagChangeListener { event ->
      logger.warn("Flag updated: ${event.key} = ${event.value}")
      handleFlagChange(event.key, event.value)
    }
  }

  fun isFeatureEnabled(feature: String): Boolean {
    return client.boolVariation(feature, false)
  }

  fun getJobConfig(jobName: String): LDValue {
    return client.jsonVariation("job-$jobName-config")
  }

  private fun handleFlagChange(key: String, value: LDValue) {
    // React to flag changes immediately
    when {
      key.startsWith("job-") && key.endsWith("-enabled") -> {
        if (value.asBooleanOrNull() == true) {
          // Start the job
        } else {
          // Stop the job
        }
      }
    }
  }

  fun shutdown() {
    client.close()
  }
}

Development

Running Tests

The project includes both unit tests and integration tests. Unit tests run against mock servers and don't require any configuration.

Integration tests make real API calls to LaunchDarkly and require valid API keys. To run integration tests:

Set the LaunchDarkly test configuration in gradle.properties:

ld.test.clientKey=your-client-side-environment-id
ld.test.mobileKey=mob-your-mobile-sdk-key
ld.test.flagKey=test-flag

The flagKey is optional and defaults to test-flag if not specified.

Integration tests are automatically skipped if the keys are not provided, so you can safely run all tests without them:

./gradlew test

Support

For issues and questions:

Project Resources

Resource Description
CODEOWNERS Outlines the project lead(s)
CODE_OF_CONDUCT.md Expected behavior for project contributors, promoting a welcoming environment
CONTRIBUTING.md Developer guide to build, test, run, access CI, chat, discuss, file issues
GOVERNANCE.md Project governance
LICENSE Apache License, Version 2.0

About

A pure java implementation of the LaunchDarkly client/mobile APIs

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published