google-site-verification: google61fe8ba583a51912.html
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