google-site-verification: google61fe8ba583a51912.html
LaunchDarkly Java SDK: Complete Feature Flagging Implementation Guide

LaunchDarkly is a feature management platform that enables you to safely deploy and control features through feature flags. This guide covers comprehensive integration with the Java SDK.


Setup and Dependencies

1. Maven Dependencies
<properties> <launchdarkly.version>6.0.0</launchdarkly.version> <spring-boot.version>3.1.0</spring-boot.version> </properties> <dependencies> <!-- LaunchDarkly SDK --> <dependency> <groupId>com.launchdarkly</groupId> <artifactId>launchdarkly-java-server-sdk</artifactId> <version>${launchdarkly.version}</version> </dependency> <!-- Optional: For Spring Boot integration --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- For HTTP client (if not using default) --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.11.0</version> </dependency> </dependencies>
2. Configuration Properties
@Configuration @ConfigurationProperties(prefix = "launchdarkly") @Data public class LaunchDarklyConfig { private String sdkKey; private boolean offline = false; private long timeoutMs = 3000; private int capacity = 10000; // Environment-specific settings private String environment = "production"; private boolean streaming = true; private long pollIntervalMs = 30000; }
# application.yml launchdarkly: sdk-key: ${LAUNCHDARKLY_SDK_KEY:sdk-12345} offline: false timeout-ms: 5000 capacity: 50000 environment: staging streaming: true poll-interval-ms: 15000

Core SDK Setup and Configuration

1. SDK Client Factory
@Component @Slf4j public class LaunchDarklyClientFactory { private final LaunchDarklyConfig config; private LDClient ldClient; public LaunchDarklyClientFactory(LaunchDarklyConfig config) { this.config = config; initializeClient(); } @PostConstruct public void initializeClient() { try { LDConfig.Builder ldConfigBuilder = new LDConfig.Builder() .dataSource(getDataSourceConfiguration()) .events(getEventsConfiguration()) .http(getHttpConfiguration()) .logging(getLoggingConfiguration()); if (config.isOffline()) { ldConfigBuilder.offline(true); } ldClient = new LDClient(config.getSdkKey(), ldConfigBuilder.build()); if (ldClient.isInitialized()) { log.info("LaunchDarkly client initialized successfully"); } else { log.error("LaunchDarkly client failed to initialize"); } } catch (Exception e) { log.error("Failed to initialize LaunchDarkly client", e); // Fallback to offline client ldClient = new LDClient(config.getSdkKey(), LDConfig.Builder.offline(true).build()); } } private Components.DataSourceFactory getDataSourceConfiguration() { if (config.isStreaming()) { return Components.streamingDataSource(); } else { return Components.pollingDataSource() .pollInterval(Duration.ofMillis(config.getPollIntervalMs())); } } private Components.EventProcessorFactory getEventsConfiguration() { return Components.sendEvents() .capacity(config.getCapacity()) .flushInterval(Duration.ofSeconds(2)); } private Components.HttpConfigurationFactory getHttpConfiguration() { return Components.httpConfiguration() .connectTimeout(Duration.ofMillis(config.getTimeoutMs())) .socketTimeout(Duration.ofMillis(config.getTimeoutMs())) .proxy(Components.httpConfiguration().proxy()); } private Components.LoggingConfigurationFactory getLoggingConfiguration() { return Components.logging(Components.slf4jSlf4j()); } public LDClient getClient() { return ldClient; } @PreDestroy public void close() { if (ldClient != null) { ldClient.close(); log.info("LaunchDarkly client closed"); } } }
2. User Context Builder
@Component public class LDUserBuilder { public LDUser.Builder createBuilder() { return new LDUser.Builder(generateAnonymousKey()); } public LDUser.Builder fromUser(User user) { return new LDUser.Builder(user.getId()) .email(user.getEmail()) .firstName(user.getFirstName()) .lastName(user.getLastName()) .custom("registrationDate", user.getRegistrationDate().toString()) .custom("tier", user.getTier()) .custom("country", user.getCountry()) .privateAttribute("email"); // Keep email private } public LDUser.Builder fromHttpRequest(HttpServletRequest request) { String userKey = extractUserKey(request); LDUser.Builder builder = new LDUser.Builder(userKey); // Add context from request builder.ip(request.getRemoteAddr()) .country(extractCountry(request)) .custom("userAgent", request.getHeader("User-Agent")) .custom("path", request.getRequestURI()) .custom("method", request.getMethod()); // Add authenticated user attributes if available String email = extractEmailFromAuth(request); if (email != null) { builder.email(email).privateAttribute("email"); } return builder; } public LDUser.Builder withCustomAttributes(LDUser.Builder builder, Map<String, String> attributes) { attributes.forEach(builder::custom); return builder; } public LDUser.Builder withPrivateAttributes(LDUser.Builder builder, String... attributes) { for (String attr : attributes) { builder.privateAttribute(attr); } return builder; } private String generateAnonymousKey() { return "anonymous-" + UUID.randomUUID().toString().substring(0, 8); } private String extractUserKey(HttpServletRequest request) { // Implementation to extract user identifier from request return request.getHeader("X-User-Id") != null ? request.getHeader("X-User-Id") : generateAnonymousKey(); } private String extractCountry(HttpServletRequest request) { // Extract from header or geolocation return request.getHeader("CF-IPCountry"); } private String extractEmailFromAuth(HttpServletRequest request) { // Extract from authentication context return null; // Implement based on your auth system } }

Feature Flag Service

1. Core Feature Flag Service
@Service @Slf4j public class FeatureFlagService { private final LDClient ldClient; private final LDUserBuilder userBuilder; // Feature flag keys - should be constants public static final String NEW_UI_FEATURE = "new-ui-feature"; public static final String PAYMENT_V2 = "payment-service-v2"; public static final String PREMIUM_FEATURES = "premium-features"; public static final String MAINTENANCE_MODE = "maintenance-mode"; public static final String BETA_FEATURES = "beta-features"; public FeatureFlagService(LDClient ldClient, LDUserBuilder userBuilder) { this.ldClient = ldClient; this.userBuilder = userBuilder; } // Basic boolean flag evaluation public boolean isEnabled(String flagKey) { return isEnabled(flagKey, userBuilder.createBuilder().build()); } public boolean isEnabled(String flagKey, LDUser user) { try { return ldClient.boolVariation(flagKey, user, false); } catch (Exception e) { log.error("Error evaluating flag {} for user {}", flagKey, user.getKey(), e); return false; // Fallback to false for safety } } public boolean isEnabled(String flagKey, HttpServletRequest request) { LDUser user = userBuilder.fromHttpRequest(request).build(); return isEnabled(flagKey, user); } public boolean isEnabled(String flagKey, User user) { LDUser ldUser = userBuilder.fromUser(user).build(); return isEnabled(flagKey, ldUser); } // String variation public String getStringVariation(String flagKey, LDUser user, String defaultValue) { try { return ldClient.stringVariation(flagKey, user, defaultValue); } catch (Exception e) { log.error("Error evaluating string flag {} for user {}", flagKey, user.getKey(), e); return defaultValue; } } // Integer variation public int getIntVariation(String flagKey, LDUser user, int defaultValue) { try { return ldClient.intVariation(flagKey, user, defaultValue); } catch (Exception e) { log.error("Error evaluating int flag {} for user {}", flagKey, user.getKey(), e); return defaultValue; } } // JSON variation public JsonNode getJsonVariation(String flagKey, LDUser user, JsonNode defaultValue) { try { return ldClient.jsonValueVariation(flagKey, user, defaultValue); } catch (Exception e) { log.error("Error evaluating JSON flag {} for user {}", flagKey, user.getKey(), e); return defaultValue; } } // Detailed evaluation with reason public EvaluationDetail<Boolean> getEvaluationDetail(String flagKey, LDUser user) { try { return ldClient.boolVariationDetail(flagKey, user, false); } catch (Exception e) { log.error("Error getting evaluation detail for flag {} for user {}", flagKey, user.getKey(), e); return EvaluationDetail.fromValue(false, EvaluationReason.error(EvaluationErrorKind.EXCEPTION)); } } // Track events for experimentation public void trackEvent(String eventName, LDUser user) { trackEvent(eventName, user, null, null); } public void trackEvent(String eventName, LDUser user, JsonNode data, Double metricValue) { try { ldClient.track(eventName, user, data, metricValue); log.debug("Tracked event: {} for user: {}", eventName, user.getKey()); } catch (Exception e) { log.error("Error tracking event {} for user {}", eventName, user.getKey(), e); } } // Check if flag exists and is valid public boolean isFlagValid(String flagKey) { try { return ldClient.isFlagKnown(flagKey); } catch (Exception e) { log.error("Error checking flag validity: {}", flagKey, e); return false; } } // Get all flags for a user public FeatureFlagsState getAllFlagsState(LDUser user) { try { return ldClient.allFlagsState(user); } catch (Exception e) { log.error("Error getting all flags for user {}", user.getKey(), e); return null; } } }
2. Specialized Flag Services
@Service public class UIFeatureService { private final FeatureFlagService flagService; public UIFeatureService(FeatureFlagService flagService) { this.flagService = flagService; } public boolean isNewUIEnabled(User user) { return flagService.isEnabled(FeatureFlagService.NEW_UI_FEATURE, user); } public boolean isBetaFeatureEnabled(User user, String feature) { LDUser ldUser = new LDUser.Builder(user.getId()) .custom("betaFeatures", String.join(",", user.getBetaFeatures())) .build(); return flagService.isEnabled(FeatureFlagService.BETA_FEATURES, ldUser); } public String getTheme(User user) { LDUser ldUser = new LDUser.Builder(user.getId()) .custom("tier", user.getTier()) .build(); return flagService.getStringVariation("ui-theme", ldUser, "light"); } } @Service public class PaymentFeatureService { private final FeatureFlagService flagService; public PaymentFeatureService(FeatureFlagService flagService) { this.flagService = flagService; } public boolean shouldUseNewPaymentService(User user, double amount) { LDUser ldUser = new LDUser.Builder(user.getId()) .custom("tier", user.getTier()) .custom("paymentAmount", String.valueOf(amount)) .custom("country", user.getCountry()) .build(); return flagService.isEnabled(FeatureFlagService.PAYMENT_V2, ldUser); } public boolean isPaymentMethodEnabled(User user, String paymentMethod) { LDUser ldUser = new LDUser.Builder(user.getId()) .custom("paymentMethod", paymentMethod) .custom("tier", user.getTier()) .build(); return flagService.isEnabled("payment-method-" + paymentMethod, ldUser); } }

Spring Boot Integration

1. Configuration Class
@Configuration @EnableConfigurationProperties(LaunchDarklyConfig.class) public class LaunchDarklyAutoConfiguration { @Bean @ConditionalOnMissingBean public LaunchDarklyClientFactory launchDarklyClientFactory(LaunchDarklyConfig config) { return new LaunchDarklyClientFactory(config); } @Bean @ConditionalOnMissingBean public LDClient ldClient(LaunchDarklyClientFactory factory) { return factory.getClient(); } @Bean @ConditionalOnMissingBean public LDUserBuilder ldUserBuilder() { return new LDUserBuilder(); } @Bean @ConditionalOnMissingBean public FeatureFlagService featureFlagService(LDClient ldClient, LDUserBuilder userBuilder) { return new FeatureFlagService(ldClient, userBuilder); } }
2. Spring MVC Interceptor
@Component public class FeatureFlagInterceptor implements HandlerInterceptor { private final FeatureFlagService flagService; private final LDUserBuilder userBuilder; public FeatureFlagInterceptor(FeatureFlagService flagService, LDUserBuilder userBuilder) { this.flagService = flagService; this.userBuilder = userBuilder; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Check maintenance mode if (flagService.isEnabled(FeatureFlagService.MAINTENANCE_MODE, request)) { if (!request.getRequestURI().startsWith("/health")) { response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value()); response.getWriter().write("Service undergoing maintenance"); return false; } } // Add feature flags to request attributes for use in controllers/views LDUser user = userBuilder.fromHttpRequest(request).build(); FeatureFlagsState flagsState = flagService.getAllFlagsState(user); if (flagsState != null) { request.setAttribute("featureFlags", flagsState); } return true; } } @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final FeatureFlagInterceptor featureFlagInterceptor; public WebMvcConfig(FeatureFlagInterceptor featureFlagInterceptor) { this.featureFlagInterceptor = featureFlagInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(featureFlagInterceptor) .addPathPatterns("/**") .excludePathPatterns("/static/**", "/health"); } }
3. Feature Flag Annotation
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface FeatureFlag { String value(); boolean fallback() default false; } @Aspect @Component public class FeatureFlagAspect { private final FeatureFlagService flagService; private final LDUserBuilder userBuilder; public FeatureFlagAspect(FeatureFlagService flagService, LDUserBuilder userBuilder) { this.flagService = flagService; this.userBuilder = userBuilder; } @Around("@annotation(featureFlag)") public Object checkFeatureFlag(ProceedingJoinPoint joinPoint, FeatureFlag featureFlag) throws Throwable { String flagKey = featureFlag.value(); boolean fallback = featureFlag.fallback(); // Extract user from method arguments or create anonymous LDUser user = extractUserFromArgs(joinPoint.getArgs()); if (flagService.isEnabled(flagKey, user)) { return joinPoint.proceed(); } else if (fallback) { return handleFallback(joinPoint, flagKey); } else { throw new FeatureNotEnabledException("Feature " + flagKey + " is not enabled for user " + user.getKey()); } } private LDUser extractUserFromArgs(Object[] args) { for (Object arg : args) { if (arg instanceof HttpServletRequest) { return userBuilder.fromHttpRequest((HttpServletRequest) arg).build(); } else if (arg instanceof User) { return userBuilder.fromUser((User) arg).build(); } else if (arg instanceof LDUser) { return (LDUser) arg; } } return userBuilder.createBuilder().build(); } private Object handleFallback(ProceedingJoinPoint joinPoint, String flagKey) { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); Class<?> returnType = method.getReturnType(); if (returnType == boolean.class) { return false; } else if (returnType == String.class) { return "fallback"; } else if (returnType.isPrimitive()) { return 0; } else { return null; } } } public class FeatureNotEnabledException extends RuntimeException { public FeatureNotEnabledException(String message) { super(message); } }

Usage Examples

1. REST Controller with Feature Flags
@RestController @RequestMapping("/api") @Slf4j public class UserController { private final UserService userService; private final FeatureFlagService flagService; private final LDUserBuilder userBuilder; public UserController(UserService userService, FeatureFlagService flagService, LDUserBuilder userBuilder) { this.userService = userService; this.flagService = flagService; this.userBuilder = userBuilder; } @GetMapping("/users/{id}") public ResponseEntity<User> getUser(@PathVariable String id, HttpServletRequest request) { User user = userService.findById(id); LDUser ldUser = userBuilder.fromHttpRequest(request).build(); // Check if new UI features should be shown if (flagService.isEnabled(FeatureFlagService.NEW_UI_FEATURE, ldUser)) { user.setUiVersion("v2"); } else { user.setUiVersion("v1"); } // Track that user was viewed with new UI if ("v2".equals(user.getUiVersion())) { flagService.trackEvent("user_viewed_v2", ldUser); } return ResponseEntity.ok(user); } @FeatureFlag(FeatureFlagService.PREMIUM_FEATURES) @GetMapping("/users/{id}/premium") public ResponseEntity<PremiumFeatures> getPremiumFeatures(@PathVariable String id) { PremiumFeatures features = userService.getPremiumFeatures(id); return ResponseEntity.ok(features); } @PostMapping("/users/{id}/payment") public ResponseEntity<PaymentResponse> processPayment(@PathVariable String id, @RequestBody PaymentRequest paymentRequest, HttpServletRequest request) { User user = userService.findById(id); LDUser ldUser = userBuilder.fromUser(user) .custom("paymentAmount", String.valueOf(paymentRequest.getAmount())) .custom("paymentMethod", paymentRequest.getMethod()) .build(); PaymentResponse response; // Route to different payment services based on feature flag if (flagService.isEnabled(FeatureFlagService.PAYMENT_V2, ldUser)) { response = userService.processPaymentV2(user, paymentRequest); flagService.trackEvent("payment_processed_v2", ldUser, JsonUtils.toJson(paymentRequest), (double) paymentRequest.getAmount()); } else { response = userService.processPaymentV1(user, paymentRequest); flagService.trackEvent("payment_processed_v1", ldUser, JsonUtils.toJson(paymentRequest), (double) paymentRequest.getAmount()); } return ResponseEntity.ok(response); } @GetMapping("/features") public ResponseEntity<Map<String, Object>> getFeaturesForUser(HttpServletRequest request) { LDUser user = userBuilder.fromHttpRequest(request).build(); FeatureFlagsState flagsState = flagService.getAllFlagsState(user); Map<String, Object> features = new HashMap<>(); if (flagsState != null) { features.put("flags", flagsState.toValuesMap()); features.put("userKey", user.getKey()); } return ResponseEntity.ok(features); } }
2. Service Layer Usage
@Service @Slf4j public class NotificationService { private final FeatureFlagService flagService; private final EmailService emailService; private final PushNotificationService pushService; public NotificationService(FeatureFlagService flagService, EmailService emailService, PushNotificationService pushService) { this.flagService = flagService; this.emailService = emailService; this.pushService = pushService; } public void sendWelcomeNotification(User user) { LDUser ldUser = new LDUser.Builder(user.getId()) .email(user.getEmail()) .custom("tier", user.getTier()) .custom("country", user.getCountry()) .build(); // Use different notification templates based on feature flag String templateKey = flagService.getStringVariation("welcome-template", ldUser, "default"); NotificationTemplate template = getTemplate(templateKey); emailService.sendWelcomeEmail(user, template); // Track which template was used flagService.trackEvent("welcome_notification_sent", ldUser, JsonUtils.toJson(Map.of("template", templateKey)), null); } @FeatureFlag("push-notifications") public void sendPushNotification(User user, String message) { LDUser ldUser = new LDUser.Builder(user.getId()) .custom("notificationType", "push") .build(); pushService.sendNotification(user, message); flagService.trackEvent("push_notification_sent", ldUser); } public void sendBatchNotifications(List<User> users, String message) { // Use percentage rollout for batch operations int enabledCount = 0; for (User user : users) { LDUser ldUser = new LDUser.Builder(user.getId()).build(); if (flagService.isEnabled("batch-notifications", ldUser)) { sendPushNotification(user, message); enabledCount++; } } log.info("Sent batch notifications to {} out of {} users", enabledCount, users.size()); } }
3. Thymeleaf Integration
@Component public class ThymeleafFeatureFlagProcessor { private final FeatureFlagService flagService; private final LDUserBuilder userBuilder; public ThymeleafFeatureFlagProcessor(FeatureFlagService flagService, LDUserBuilder userBuilder) { this.flagService = flagService; this.userBuilder = userBuilder; } public boolean isEnabled(String flagKey, HttpServletRequest request) { LDUser user = userBuilder.fromHttpRequest(request).build(); return flagService.isEnabled(flagKey, user); } } @ControllerAdvice public class FeatureFlagControllerAdvice { private final ThymeleafFeatureFlagProcessor flagProcessor; public FeatureFlagControllerAdvice(ThymeleafFeatureFlagProcessor flagProcessor) { this.flagProcessor = flagProcessor; } @ModelAttribute("featureFlags") public ThymeleafFeatureFlagProcessor featureFlags() { return flagProcessor; } }
<!-- In Thymeleaf template --> <div th:if="${@featureFlags.isEnabled('new-ui-feature', request)}"> <div class="new-ui-component"> <!-- New UI content --> </div> </div> <div th:unless="${@featureFlags.isEnabled('new-ui-feature', request)}"> <div class="old-ui-component"> <!-- Old UI content --> </div> </div>

Testing and Monitoring

1. Test Configuration
@TestConfiguration public class LaunchDarklyTestConfig { @Bean @Primary public LDClient testLDClient() { // Use in-memory test client LDConfig config = new LDConfig.Builder() .dataSource(Components.externalUpdatesOnly()) .events(Components.noEvents()) .build(); return new LDClient("test-sdk-key", config); } @Bean @Primary public FeatureFlagService testFeatureFlagService(LDClient ldClient, LDUserBuilder userBuilder) { return new FeatureFlagService(ldClient, userBuilder); } } @SpringBootTest @TestPropertySource(properties = "launchdarkly.offline=true") class FeatureFlagServiceTest { @Autowired private FeatureFlagService flagService; @Autowired private LDClient ldClient; @Test void testFeatureFlagEvaluation() { // Given LDUser user = new LDUser.Builder("test-user").build(); // When/Then - test with offline client assertThat(flagService.isEnabled("test-flag", user)).isFalse(); } }
2. Health Check
@Component public class LaunchDarklyHealthIndicator implements HealthIndicator { private final LDClient ldClient; public LaunchDarklyHealthIndicator(LDClient ldClient) { this.ldClient = ldClient; } @Override public Health health() { try { boolean initialized = ldClient.isInitialized(); boolean valid = ldClient.isOffline() || initialized; if (valid) { return Health.up() .withDetail("initialized", initialized) .withDetail("offline", ldClient.isOffline()) .build(); } else { return Health.down() .withDetail("error", "LaunchDarkly client not initialized") .build(); } } catch (Exception e) { return Health.down(e).build(); } } }
3. Metrics and Monitoring
@Component public class LaunchDarklyMetrics { private final FeatureFlagService flagService; private final MeterRegistry meterRegistry; private final Counter flagEvaluationCounter; private final Timer flagEvaluationTimer; public LaunchDarklyMetrics(FeatureFlagService flagService, MeterRegistry meterRegistry) { this.flagService = flagService; this.meterRegistry = meterRegistry; this.flagEvaluationCounter = Counter.builder("launchdarkly.flag.evaluations") .description("Number of feature flag evaluations") .register(meterRegistry); this.flagEvaluationTimer = Timer.builder("launchdarkly.flag.evaluation.time") .description("Time taken for feature flag evaluation") .register(meterRegistry); } public boolean trackFlagEvaluation(String flagKey, LDUser user, Supplier<Boolean> evaluation) { return flagEvaluationTimer.record(() -> { boolean result = evaluation.get(); flagEvaluationCounter.increment(); // Tag with flag key and result meterRegistry.counter("launchdarkly.flag.evaluations.result", "flag", flagKey, "result", String.valueOf(result) ).increment(); return result; }); } }

Best Practices

  1. Flag Key Management: Use constants for flag keys to avoid typos
  2. Fallback Strategies: Always provide safe fallbacks for flag evaluations
  3. User Context: Build rich user contexts for targeted rollouts
  4. Cleanup: Remove flags once they're fully rolled out
  5. Monitoring: Track flag evaluations and their business impact
  6. Testing: Test both enabled and disabled flag states
  7. Documentation: Document flag purpose, rollout plan, and cleanup criteria

This comprehensive LaunchDarkly Java SDK integration provides a robust foundation for feature flag management, enabling safe deployments, gradual rollouts, and data-driven feature decisions.

Leave a Reply

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


Macro Nepal Helper