Dynamic Feature Management: Implementing Feature Flags with LaunchDarkly in Java

Feature flags (or feature toggles) have become an essential practice in modern software development, enabling teams to release features safely, conduct experiments, and manage functionality without code deployments. LaunchDarkly is a leading feature management platform that provides sophisticated control over feature flags across your applications. This guide explores how to effectively integrate LaunchDarkly into Java applications for powerful feature management.


What are Feature Flags?

Feature flags are conditional statements that allow you to turn features on or off at runtime without deploying new code. They enable:

  • Trunk-Based Development: Merge code to main branch daily while hiding incomplete features
  • Canary Releases: Gradually roll out features to specific user segments
  • A/B Testing: Test different variations of features
  • Kill Switches: Quickly disable problematic features in production
  • Environment-Specific Configuration: Different settings per environment

LaunchDarkly Key Concepts

1. Flags: Boolean or multivariate configuration points
2. Environments: Different contexts (dev, staging, prod)
3. Targeting: Rules for enabling features for specific users
4. Contexts: Entities that receive flag values (users, organizations, devices)
5. SDKs: Client libraries that evaluate flags


Setting Up LaunchDarkly

1. Create LaunchDarkly Account

2. Get SDK Key

// SDK keys are environment-specific
// Production: found in Project settings > Environments
// Development: found in your environment settings
String SDK_KEY = "sdk-12345678-1234-1234-1234-123456789012";

3. Add Maven Dependency

<dependency>
<groupId>com.launchdarkly</groupId>
<artifactId>launchdarkly-java-server-sdk</artifactId>
<version>6.3.0</version>
</dependency>

4. Add Gradle Dependency

implementation 'com.launchdarkly:launchdarkly-java-server-sdk:6.3.0'

Basic SDK Integration

1. Initialize the LDClient

import com.launchdarkly.sdk.server.*;
public class LaunchDarklyService {
private static LDClient ldClient;
public static void initialize(String sdkKey) {
LDConfig config = new LDConfig.Builder()
.events(Components.noEvents()) // Disable analytics in dev
.build();
ldClient = new LDClient(sdkKey, config);
}
public static void shutdown() {
if (ldClient != null) {
ldClient.close();
}
}
}

2. Spring Boot Configuration

@Configuration
public class LaunchDarklyConfig {
@Value("${launchdarkly.sdk-key:}")
private String sdkKey;
@Bean
public LDClient ldClient() {
if (sdkKey.isEmpty()) {
// Return mock client for local development
return new LDClient("dummy-key");
}
LDConfig config = new LDConfig.Builder()
.events(Components.sendEvents())
.build();
return new LDClient(sdkKey, config);
}
}

Implementing Feature Flags

1. Boolean Flags (On/Off Features)

@Service
public class FeatureToggleService {
private final LDClient ldClient;
public FeatureToggleService(LDClient ldClient) {
this.ldClient = ldClient;
}
public boolean isNewUIFeatureEnabled(String userId) {
LDContext context = LDContext.builder("user-key", userId)
.name("User " + userId)
.build();
return ldClient.boolVariation("new-ui-enabled", context, false);
}
public boolean isPaymentFeatureEnabled(User user) {
LDContext context = LDContext.builder("user-key", user.getId())
.name(user.getFullName())
.set("email", user.getEmail())
.set("tier", user.getPlanTier()) // "basic", "premium", "enterprise"
.set("registrationDate", user.getRegistrationDate().toString())
.build();
return ldClient.boolVariation("new-payment-system", context, false);
}
}

2. Multivariate Flags (Multiple Variations)

@Service
public class PricingService {
private final LDClient ldClient;
public enum PricingTier {
STANDARD, PREMIUM, ENTERPRISE
}
public PricingTier getPricingTier(String userId, String companySize) {
LDContext context = LDContext.builder("user-key", userId)
.set("companySize", companySize) // "startup", "sme", "enterprise"
.build();
String tierValue = ldClient.stringVariation(
"pricing-model", 
context, 
"STANDARD" // default value
);
return PricingTier.valueOf(tierValue);
}
public double getDiscountRate(String userId) {
LDContext context = LDContext.builder("user-key", userId).build();
// For numeric values
int discount = ldClient.intVariation("discount-percentage", context, 0);
return discount / 100.0;
}
}

3. JSON Flag Variations

@Service
public class ConfigurationService {
private final LDClient ldClient;
private final ObjectMapper objectMapper;
public FeatureConfig getFeatureConfig(String userId) {
LDContext context = LDContext.builder("user-key", userId).build();
LDValue jsonValue = ldClient.jsonValueVariation(
"feature-config", 
context, 
LDValue.buildObject().build()
);
try {
return objectMapper.readValue(
jsonValue.toJsonString(), 
FeatureConfig.class
);
} catch (Exception e) {
return new FeatureConfig(); // fallback
}
}
public static class FeatureConfig {
private int maxResults = 50;
private boolean advancedSearch = false;
private String theme = "light";
private List<String> enabledModules = Arrays.asList("basic");
// getters and setters
}
}

Advanced Context Types

1. Multi-context Evaluation (Users + Organizations)

public boolean isEnterpriseFeatureEnabled(User user, Organization org) {
LDContext userContext = LDContext.builder("user", user.getId())
.set("email", user.getEmail())
.set("role", user.getRole())
.build();
LDContext orgContext = LDContext.builder("organization", org.getId())
.set("name", org.getName())
.set("tier", org.getTier())
.set("employeeCount", org.getEmployeeCount())
.build();
// Combine contexts
LDContext multiContext = LDContext.createMulti(
userContext,
orgContext
);
return ldClient.boolVariation("enterprise-dashboard", multiContext, false);
}

2. Anonymous Users

public boolean isFeatureEnabledForAnonymous(String sessionId) {
LDContext context = LDContext.builder("anonymous", sessionId)
.anonymous(true)
.build();
return ldClient.boolVariation("public-feature", context, false);
}

Spring Boot Integration Examples

1. Controller with Feature Flags

@RestController
@RequestMapping("/api/v2")
public class NewFeatureController {
private final FeatureToggleService featureService;
private final UserService userService;
public NewFeatureController(FeatureToggleService featureService, 
UserService userService) {
this.featureService = featureService;
this.userService = userService;
}
@GetMapping("/dashboard")
public ResponseEntity<?> getDashboard(@RequestHeader("User-Id") String userId) {
User user = userService.findUserById(userId);
if (!featureService.isNewUIFeatureEnabled(userId)) {
return fallbackToOldDashboard(user);
}
// New feature logic
DashboardData data = dashboardService.getEnhancedDashboard(user);
return ResponseEntity.ok(data);
}
@PostMapping("/payment")
public ResponseEntity<PaymentResult> processPayment(
@RequestBody PaymentRequest request,
@RequestHeader("User-Id") String userId) {
User user = userService.findUserById(userId);
if (featureService.isPaymentFeatureEnabled(user)) {
// New payment system
return ResponseEntity.ok(newPaymentService.process(request));
} else {
// Legacy payment system
return ResponseEntity.ok(legacyPaymentService.process(request));
}
}
}

2. Conditional Bean Configuration

@Configuration
public class ServiceConfiguration {
@Bean
@ConditionalOnFeatureFlag(value = "new-search-service", defaultValue = false)
public SearchService newSearchService() {
return new NewSearchService();
}
@Bean
@ConditionalOnFeatureFlag(value = "new-search-service", defaultValue = false, negate = true)
public SearchService legacySearchService() {
return new LegacySearchService();
}
}
// Custom conditional annotation
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(FeatureFlagCondition.class)
public @interface ConditionalOnFeatureFlag {
String value();
boolean defaultValue() default false;
boolean negate() default false;
}
public class FeatureFlagCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(
ConditionalOnFeatureFlag.class.getName()
);
String flagKey = (String) attributes.get("value");
boolean defaultValue = (Boolean) attributes.get("defaultValue");
boolean negate = (Boolean) attributes.get("negate");
// Implement flag evaluation logic
boolean flagValue = evaluateFlag(flagKey, defaultValue);
return negate ? !flagValue : flagValue;
}
}

A/B Testing Implementation

@Service
public class ExperimentService {
private final LDClient ldClient;
public enum ButtonColor {
BLUE, GREEN, RED, ORANGE
}
public ButtonColor getButtonColorForUser(String userId) {
LDContext context = LDContext.builder("user-key", userId)
.set("segment", getUserSegment(userId))
.build();
String color = ldClient.stringVariation(
"button-color-experiment", 
context, 
"BLUE"
);
return ButtonColor.valueOf(color);
}
public String getPromotionalMessage(String userId) {
LDContext context = LDContext.builder("user-key", userId).build();
LDValue messageConfig = ldClient.jsonValueVariation(
"promo-message-test",
context,
LDValue.parse("{\"message\": \"Special Offer!\", \"discount\": 10}")
);
return String.format(
"%s - %d%% off!", 
messageConfig.get("message").stringValue(),
messageConfig.get("discount").intValue()
);
}
}

Testing and Development

1. Test Configuration

@ExtendWith(MockitoExtension.class)
class FeatureToggleServiceTest {
@Mock
private LDClient ldClient;
@InjectMocks
private FeatureToggleService featureService;
@Test
void shouldReturnTrueWhenFeatureEnabled() {
// Given
String userId = "user123";
LDContext context = LDContext.builder("user-key", userId).build();
when(ldClient.boolVariation("new-ui-enabled", context, false))
.thenReturn(true);
// When
boolean result = featureService.isNewUIFeatureEnabled(userId);
// Then
assertTrue(result);
}
@Test
void shouldUseDefaultValueWhenFlagNotFound() {
// Given
String userId = "user123";
LDContext context = LDContext.builder("user-key", userId).build();
when(ldClient.boolVariation("non-existent-flag", context, true))
.thenReturn(true);
// When
boolean result = featureService.isFlagEnabled("non-existent-flag", userId, true);
// Then
assertTrue(result);
}
}

2. Mock LDClient for Local Development

@Profile("local")
@Configuration
public class LocalLaunchDarklyConfig {
@Bean
public LDClient ldClient() {
// Use a test-specific configuration that doesn't connect to LaunchDarkly
LDConfig config = new LDConfig.Builder()
.offline(true)
.build();
return new LDClient("dummy-sdk-key", config);
}
}

3. Feature Flag Test Harness

@Component
public class FeatureFlagTestHarness {
private final LDClient ldClient;
private final Map<String, Boolean> flagOverrides = new ConcurrentHashMap<>();
public FeatureFlagTestHarness(LDClient ldClient) {
this.ldClient = ldClient;
}
@PostConstruct
public void setupTestFlags() {
// Set up default test values
if (isTestEnvironment()) {
setFlagOverride("new-ui-enabled", true);
setFlagOverride("new-payment-system", false);
}
}
public void setFlagOverride(String flagKey, boolean value) {
flagOverrides.put(flagKey, value);
}
public boolean getFlagValue(String flagKey, String userId, boolean defaultValue) {
// Use overrides in test environment
if (flagOverrides.containsKey(flagKey)) {
return flagOverrides.get(flagKey);
}
// Otherwise use real LD client
LDContext context = LDContext.builder("user-key", userId).build();
return ldClient.boolVariation(flagKey, context, defaultValue);
}
}

Best Practices

1. Flag Naming Convention

// Good flag names
"billing-new-ui"
"search-algorithm-v2"
"checkout-experiment-2024"
// Avoid
"flag1", "test", "new_feature"

2. Centralized Flag Management

@Component
public class FeatureFlags {
public static final String NEW_UI = "new-ui-enabled";
public static final String ADVANCED_SEARCH = "advanced-search";
public static final String PAYMENT_V2 = "payment-system-v2";
public static final String DASHBOARD_REDESIGN = "dashboard-redesign-2024";
private final LDClient ldClient;
public boolean isEnabled(String flagKey, LDContext context, boolean defaultValue) {
return ldClient.boolVariation(flagKey, context, defaultValue);
}
}

3. Cleanup and Maintenance

@Service
public class FlagCleanupService {
private static final Logger logger = LoggerFactory.getLogger(FlagCleanupService.class);
public void monitorFlagUsage() {
// Log flag evaluations for audit
// Remove flags that are 100% rolled out
// Clean up temporary experiment flags
}
@EventListener
public void onApplicationShutdown(ContextClosedEvent event) {
logger.info("Closing LaunchDarkly client...");
// Proper cleanup
}
}

Monitoring and Analytics

1. Tracking Flag Evaluations

@Aspect
@Component
public class FeatureFlagMetricsAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(TrackFeatureFlag)")
public Object trackFlagUsage(ProceedingJoinPoint joinPoint) throws Throwable {
String flagName = getFlagName(joinPoint);
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
recordMetric(flagName, "success", System.currentTimeMillis() - startTime);
return result;
} catch (Exception e) {
recordMetric(flagName, "error", System.currentTimeMillis() - startTime);
throw e;
}
}
private void recordMetric(String flagName, String status, long duration) {
Counter.builder("feature.flag.evaluation")
.tag("flag", flagName)
.tag("status", status)
.register(meterRegistry)
.increment();
Timer.builder("feature.flag.duration")
.tag("flag", flagName)
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}
}

Conclusion

LaunchDarkly with Java provides a powerful foundation for implementing feature flags that enable:

  • Safe Deployments: Release features with zero downtime
  • Progressive Delivery: Gradually roll out features to specific user segments
  • Data-Driven Decisions: A/B test features before full rollout
  • Operational Control: Quickly disable problematic features
  • Team Collaboration: Enable different teams to work on features independently

By following the patterns and best practices outlined in this guide, you can build robust, flexible Java applications that can adapt to changing requirements and user needs without requiring code deployments. The key to success is starting simple, establishing clear flag management processes, and gradually incorporating more advanced targeting and experimentation as your feature management maturity grows.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper