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.
- 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
This repository contains two modules:
ld-java-client- Core JVM Client SDK for LaunchDarklyld-gradle-value-source- Gradle ValueSource implementations for using flags to control build behavior
Add the SDK to your build.gradle(.kts) file:
dependencies {
implementation("xyz.block.launchdarkly:ld-java-client:0.1.0")
}// 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()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.
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)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()// 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()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()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")| 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 |
This SDK provides similar functionality to the mobile SDKs but runs on any JVM platform (server, CLI tools, desktop apps, etc.), not just Android.
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
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()
}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()
}
}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()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()
}
}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-flagThe 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 testFor issues and questions:
- GitHub Issues: https://github.com/block/ld-java-client/issues
- LaunchDarkly Documentation: https://docs.launchdarkly.com/
| 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 |