Feature flags (feature toggles) allow you to deploy code safely and control feature releases without redeploying your application. Unleash is an open-source feature flag service that provides advanced rollout strategies and management capabilities.
Core Concepts
What are Feature Flags?
- Conditional logic that controls feature availability
- Runtime configuration without code changes
- Safe deployment practices (dark launches, canary releases)
- A/B testing and gradual rollouts
Why Use Unleash?
- Open-source with enterprise features
- Advanced rollout strategies
- Client SDKs for multiple languages
- Dashboard for management
- Experimentation and metrics
Dependencies and Setup
1. Maven Dependencies
<properties> <unleash.version>5.0.5</unleash.version> <spring-boot.version>3.1.0</spring-boot.version> </properties> <dependencies> <!-- Unleash Java Client --> <dependency> <groupId>io.getunleash</groupId> <artifactId>unleash-client-java</artifactId> <version>${unleash.version}</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- For configuration --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> </dependencies> 2. Running Unleash Server
# Using Docker docker run --name unleash -e DATABASE_URL=postgres://unleash:password@postgres:5432/unleash -p 4242:4242 unleashorg/unleash-server # Or use Unleash Open Source docker run --name unleash -e DATABASE_URL=sqlite:///unleash.sqlite -p 4242:4242 unleashorg/unleash-server
Core Implementation
1. Configuration Properties
@ConfigurationProperties(prefix = "unleash") @Data public class UnleashProperties { private String appName = "my-application"; private String instanceId; private String environment = "development"; private String apiUrl = "http://localhost:4242/api"; private String apiToken; private long fetchTogglesInterval = 10; // seconds private long sendMetricsInterval = 60; // seconds private boolean synchronousFetchOnInitialisation = true; private boolean backupFile = true; private String backupLocation = ".unleash"; public String getInstanceId() { if (instanceId == null || instanceId.trim().isEmpty()) { return appName + "-" + UUID.randomUUID().toString().substring(0, 8); } return instanceId; } } 2. Unleash Configuration
@Configuration @EnableConfigurationProperties(UnleashProperties.class) @Slf4j public class UnleashConfiguration { private static final String BACKUP_FILE = "unleash-backup.json"; @Bean @Primary public Unleash unleash(UnleashProperties properties) { try { UnleashConfig config = UnleashConfig.builder() .appName(properties.getAppName()) .instanceId(properties.getInstanceId()) .unleashAPI(properties.getApiUrl()) .apiKey(properties.getApiToken()) .environment(properties.getEnvironment()) .fetchTogglesInterval(properties.getFetchTogglesInterval()) .sendMetricsInterval(properties.getSendMetricsInterval()) .synchronousFetchOnInitialisation(properties.isSynchronousFetchOnInitialisation()) .backupFile(properties.isBackupFile() ? new File(properties.getBackupLocation(), BACKUP_FILE) : null) .build(); Unleash unleash = new DefaultUnleash(config); // Add event listeners for monitoring unleash.getUnleashContextProvider().addListener((oldContext, newContext) -> { log.info("Unleash context changed: {}", newContext); }); log.info("Unleash client initialized for app: {}", properties.getAppName()); return unleash; } catch (Exception e) { log.error("Failed to initialize Unleash client, using fallback", e); return createFallbackUnleash(); } } private Unleash createFallbackUnleash() { return new DefaultUnleash(UnleashConfig.builder() .appName("fallback-app") .unleashAPI("http://invalid") .build()); } @Bean public UnleashContextProvider unleashContextProvider() { return new DefaultUnleashContextProvider(); } } 3. Feature Flag Service
@Service @Slf4j public class FeatureFlagService { private final Unleash unleash; private final UnleashContextProvider contextProvider; public FeatureFlagService(Unleash unleash, UnleashContextProvider contextProvider) { this.unleash = unleash; this.contextProvider = contextProvider; } // Basic feature flag check public boolean isEnabled(String featureName) { return unleash.isEnabled(featureName); } // Feature flag check with context public boolean isEnabled(String featureName, UnleashContext context) { return unleash.isEnabled(featureName, context); } // Feature flag check for user public boolean isEnabledForUser(String featureName, String userId) { UnleashContext context = UnleashContext.builder() .userId(userId) .build(); return unleash.isEnabled(featureName, context); } // Feature flag check for session public boolean isEnabledForSession(String featureName, String sessionId) { UnleashContext context = UnleashContext.builder() .sessionId(sessionId) .build(); return unleash.isEnabled(featureName, context); } // Feature flag with advanced context public boolean isEnabled(String featureName, String userId, String sessionId, Map<String, String> properties) { UnleashContext context = UnleashContext.builder() .userId(userId) .sessionId(sessionId) .properties(properties) .build(); return unleash.isEnabled(featureName, context); } // Get variant for A/B testing public Variant getVariant(String featureName, UnleashContext context) { return unleash.getVariant(featureName, context); } public Variant getVariant(String featureName, String userId) { UnleashContext context = UnleashContext.builder() .userId(userId) .build(); return unleash.getVariant(featureName, context); } // Get all feature flags (for debugging/admin) public Map<String, Boolean> getAllFeatures() { return unleash.more().getFeatureToggleNames().stream() .collect(Collectors.toMap( name -> name, this::isEnabled )); } public Map<String, Object> getFeatureDetails(String featureName) { Map<String, Object> details = new HashMap<>(); details.put("enabled", isEnabled(featureName)); details.put("variant", getVariant(featureName, UnleashContext.builder().build())); // Add more details if available through reflection try { // This is a hack to get more details - in real implementation, // you might extend the Unleash client Field togglesField = unleash.getClass().getDeclaredField("toggleRepository"); togglesField.setAccessible(true); Object toggleRepository = togglesField.get(unleash); // Additional reflection to get toggle details // This is simplified - in practice, you'd need proper error handling } catch (Exception e) { log.debug("Could not get detailed feature info: {}", e.getMessage()); } return details; } // Force refresh from server public void refreshToggles() { unleash.more().fetchToggles(); } } 4. Unleash Context Provider
@Component @Slf4j public class DefaultUnleashContextProvider implements UnleashContextProvider { private static final ThreadLocal<UnleashContext> contextHolder = new ThreadLocal<>(); @Override public UnleashContext getContext() { UnleashContext context = contextHolder.get(); if (context == null) { // Create default context context = UnleashContext.builder() .environment(System.getProperty("spring.profiles.active", "default")) .appName("my-application") .currentTime(Instant.now()) .build(); } return context; } public void setContext(UnleashContext context) { contextHolder.set(context); } public void clearContext() { contextHolder.remove(); } // Helper methods for common context scenarios public void setUserContext(String userId, Map<String, String> properties) { UnleashContext context = UnleashContext.builder() .userId(userId) .properties(properties) .currentTime(Instant.now()) .build(); setContext(context); } public void setSessionContext(String sessionId, String remoteAddress) { UnleashContext context = UnleashContext.builder() .sessionId(sessionId) .remoteAddress(remoteAddress) .currentTime(Instant.now()) .build(); setContext(context); } } Spring Boot Integration
1. Feature Flag Annotations
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface FeatureToggle { String value(); String fallbackMethod() default ""; } @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface FeatureVariant { String feature(); String variant(); String fallbackMethod() default ""; } 2. Feature Flag AOP
@Aspect @Component @Slf4j public class FeatureFlagAspect { private final FeatureFlagService featureFlagService; private final ApplicationContext applicationContext; public FeatureFlagAspect(FeatureFlagService featureFlagService, ApplicationContext applicationContext) { this.featureFlagService = featureFlagService; this.applicationContext = applicationContext; } @Around("@annotation(featureToggle)") public Object checkFeatureToggle(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) throws Throwable { String featureName = featureToggle.value(); if (featureFlagService.isEnabled(featureName)) { log.debug("Feature '{}' is enabled, executing method", featureName); return joinPoint.proceed(); } else { log.debug("Feature '{}' is disabled, executing fallback", featureName); return executeFallback(joinPoint, featureToggle.fallbackMethod()); } } @Around("@annotation(featureVariant)") public Object checkFeatureVariant(ProceedingJoinPoint joinPoint, FeatureVariant featureVariant) throws Throwable { String featureName = featureVariant.feature(); String expectedVariant = featureVariant.variant(); Variant currentVariant = featureFlagService.getVariant(featureName, featureFlagService.getContextProvider().getContext()); if (expectedVariant.equals(currentVariant.getName())) { log.debug("Variant '{}' for feature '{}' is active", expectedVariant, featureName); return joinPoint.proceed(); } else { log.debug("Variant '{}' for feature '{}' is not active, current: {}", expectedVariant, featureName, currentVariant.getName()); return executeFallback(joinPoint, featureVariant.fallbackMethod()); } } private Object executeFallback(ProceedingJoinPoint joinPoint, String fallbackMethod) throws Throwable { if (fallbackMethod == null || fallbackMethod.trim().isEmpty()) { return null; // Or throw exception based on your needs } // Execute fallback method on the same class Object target = joinPoint.getTarget(); Method method = findFallbackMethod(target.getClass(), fallbackMethod, joinPoint.getArgs()); if (method != null) { method.setAccessible(true); return method.invoke(target, joinPoint.getArgs()); } else { log.warn("Fallback method '{}' not found", fallbackMethod); return null; } } private Method findFallbackMethod(Class<?> clazz, String methodName, Object[] args) { return Arrays.stream(clazz.getDeclaredMethods()) .filter(method -> method.getName().equals(methodName)) .filter(method -> method.getParameterCount() == args.length) .findFirst() .orElse(null); } } 3. Web Context Interceptor
@Component public class UnleashWebInterceptor implements HandlerInterceptor { private final UnleashContextProvider contextProvider; public UnleashWebInterceptor(UnleashContextProvider contextProvider) { this.contextProvider = contextProvider; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Extract user context from request String userId = extractUserId(request); String sessionId = request.getSession().getId(); String remoteAddress = request.getRemoteAddr(); String userAgent = request.getHeader("User-Agent"); Map<String, String> properties = new HashMap<>(); properties.put("userAgent", userAgent); properties.put("remoteAddress", remoteAddress); // Set Unleash context for this request UnleashContext context = UnleashContext.builder() .userId(userId) .sessionId(sessionId) .remoteAddress(remoteAddress) .properties(properties) .build(); contextProvider.setContext(context); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // Clean up context to avoid memory leaks contextProvider.clearContext(); } private String extractUserId(HttpServletRequest request) { // Implement your user extraction logic // This could be from JWT, session, header, etc. String userId = request.getHeader("X-User-Id"); if (userId == null && request.getUserPrincipal() != null) { userId = request.getUserPrincipal().getName(); } return userId != null ? userId : "anonymous"; } } @Configuration public class WebConfig implements WebMvcConfigurer { private final UnleashWebInterceptor unleashInterceptor; public WebConfig(UnleashWebInterceptor unleashInterceptor) { this.unleashInterceptor = unleashInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(unleashInterceptor) .addPathPatterns("/**") .excludePathPatterns("/actuator/**"); } } Usage Examples
1. Service with Feature Flags
@Service @Slf4j public class PaymentService { private final FeatureFlagService featureFlagService; private final UnleashContextProvider contextProvider; public PaymentService(FeatureFlagService featureFlagService, UnleashContextProvider contextProvider) { this.featureFlagService = featureFlagService; this.contextProvider = contextProvider; } // Basic feature flag usage public ProcessPaymentResult processPayment(PaymentRequest request) { if (featureFlagService.isEnabled("new-payment-processor")) { log.info("Using new payment processor"); return processWithNewProcessor(request); } else { log.info("Using legacy payment processor"); return processWithLegacyProcessor(request); } } // User-specific feature flag public boolean canUseAdvancedFeatures(String userId) { return featureFlagService.isEnabledForUser("advanced-features", userId); } // A/B testing with variants public String getRecommendedTheme(String userId) { Variant variant = featureFlagService.getVariant("theme-test", userId); switch (variant.getName()) { case "dark": return "dark-theme"; case "light": return "light-theme"; default: return "default-theme"; } } // Annotation-based feature toggle @FeatureToggle(value = "advanced-analytics", fallbackMethod = "getBasicAnalytics") public AnalyticsData getAdvancedAnalytics(String userId) { log.info("Generating advanced analytics for user: {}", userId); // Complex analytics logic return new AnalyticsData(/* advanced data */); } public AnalyticsData getBasicAnalytics(String userId) { log.info("Generating basic analytics for user: {}", userId); // Simple analytics logic return new AnalyticsData(/* basic data */); } // Variant-based feature @FeatureVariant(feature = "checkout-experiment", variant = "single-page", fallbackMethod = "multiPageCheckout") public CheckoutResult singlePageCheckout(Cart cart) { log.info("Using single-page checkout"); // Single page checkout logic return new CheckoutResult(/* result */); } public CheckoutResult multiPageCheckout(Cart cart) { log.info("Using multi-page checkout"); // Multi-page checkout logic return new CheckoutResult(/* result */); } // Gradual rollout based on user ID public boolean shouldUseNewSearchAlgorithm(String userId) { Map<String, String> properties = new HashMap<>(); properties.put("beta-tester", isBetaTester(userId) ? "true" : "false"); return featureFlagService.isEnabled("new-search-algorithm", userId, null, properties); } private boolean isBetaTester(String userId) { // Check if user is in beta tester group return userId.hashCode() % 10 == 0; // Simple example } private ProcessPaymentResult processWithNewProcessor(PaymentRequest request) { // New payment processing logic return new ProcessPaymentResult(true, "processed-with-new"); } private ProcessPaymentResult processWithLegacyProcessor(PaymentRequest request) { // Legacy payment processing logic return new ProcessPaymentResult(true, "processed-with-legacy"); } } 2. REST Controller with Feature Flags
@RestController @RequestMapping("/api") @Slf4j public class FeatureAwareController { private final FeatureFlagService featureFlagService; private final PaymentService paymentService; private final UserService userService; public FeatureAwareController(FeatureFlagService featureFlagService, PaymentService paymentService, UserService userService) { this.featureFlagService = featureFlagService; this.paymentService = paymentService; this.userService = userService; } @GetMapping("/features") public ResponseEntity<Map<String, Object>> getFeatureFlags( @RequestHeader(value = "X-User-Id", required = false) String userId) { Map<String, Object> response = new HashMap<>(); response.put("allFeatures", featureFlagService.getAllFeatures()); if (userId != null) { Map<String, Boolean> userFeatures = new HashMap<>(); userFeatures.put("advanced-features", featureFlagService.isEnabledForUser("advanced-features", userId)); userFeatures.put("new-ui", featureFlagService.isEnabledForUser("new-ui", userId)); response.put("userFeatures", userFeatures); } return ResponseEntity.ok(response); } @PostMapping("/payments") public ResponseEntity<ProcessPaymentResult> processPayment( @RequestBody PaymentRequest request, @RequestHeader(value = "X-User-Id") String userId) { // Check if user can use new payment features if (featureFlagService.isEnabledForUser("new-payment-methods", userId)) { log.info("User {} can use new payment methods", userId); } ProcessPaymentResult result = paymentService.processPayment(request); return ResponseEntity.ok(result); } @GetMapping("/user/{userId}/theme") public ResponseEntity<ThemeResponse> getUserTheme(@PathVariable String userId) { String theme = paymentService.getRecommendedTheme(userId); return ResponseEntity.ok(new ThemeResponse(userId, theme)); } @GetMapping("/search") public ResponseEntity<SearchResults> search( @RequestParam String query, @RequestHeader(value = "X-User-Id") String userId) { boolean useNewAlgorithm = paymentService.shouldUseNewSearchAlgorithm(userId); SearchResults results; if (useNewAlgorithm) { log.info("Using new search algorithm for user: {}", userId); results = userService.advancedSearch(query, userId); } else { log.info("Using legacy search algorithm for user: {}", userId); results = userService.basicSearch(query); } return ResponseEntity.ok(results); } @GetMapping("/analytics/{userId}") public ResponseEntity<AnalyticsData> getAnalytics(@PathVariable String userId) { // This will automatically use the appropriate method based on feature flag AnalyticsData data = paymentService.getAdvancedAnalytics(userId); return ResponseEntity.ok(data); } } 3. Configuration-Based Feature Flags
@Configuration @Slf4j public class FeatureFlagConfiguration { private final FeatureFlagService featureFlagService; public FeatureFlagConfiguration(FeatureFlagService featureFlagService) { this.featureFlagService = featureFlagService; } @Bean @ConditionalOnFeatureFlag("new-cache-provider") public CacheManager redisCacheManager() { log.info("Initializing Redis cache manager (new implementation)"); // Redis cache implementation return new RedisCacheManager(/* config */); } @Bean @ConditionalOnMissingBean(CacheManager.class) public CacheManager ehCacheManager() { log.info("Initializing EhCache cache manager (legacy implementation)"); // EhCache implementation return new EhCacheCacheManager(/* config */); } @Bean @ConditionalOnFeatureFlag(value = "circuit-breaker", havingVariant = "resilience4j") public CircuitBreaker resilience4jCircuitBreaker() { log.info("Initializing Resilience4j circuit breaker"); return Resilience4jCircuitBreaker.create(); } @Bean @ConditionalOnFeatureFlag(value = "circuit-breaker", havingVariant = "hystrix") public CircuitBreaker hystrixCircuitBreaker() { log.info("Initializing Hystrix circuit breaker"); return HystrixCircuitBreaker.create(); } } // Custom condition for feature flags @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Conditional(OnFeatureFlagCondition.class) public @interface ConditionalOnFeatureFlag { String value(); String havingVariant() default ""; } public class OnFeatureFlagCondition implements Condition { private final FeatureFlagService featureFlagService; public OnFeatureFlagCondition() { // In real implementation, this would be injected this.featureFlagService = ApplicationContextHolder.getBean(FeatureFlagService.class); } @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Map<String, Object> attributes = metadata.getAnnotationAttributes( ConditionalOnFeatureFlag.class.getName()); if (attributes == null) return false; String featureName = (String) attributes.get("value"); String variant = (String) attributes.get("havingVariant"); if (variant != null && !variant.isEmpty()) { Variant currentVariant = featureFlagService.getVariant(featureName, featureFlagService.getContextProvider().getContext()); return variant.equals(currentVariant.getName()); } else { return featureFlagService.isEnabled(featureName); } } } 4. Admin/Management Endpoints
@RestController @RequestMapping("/admin/features") @Slf4j public class FeatureFlagAdminController { private final FeatureFlagService featureFlagService; private final Unleash unleash; public FeatureFlagAdminController(FeatureFlagService featureFlagService, Unleash unleash) { this.featureFlagService = featureFlagService; this.unleash = unleash; } @GetMapping public ResponseEntity<Map<String, Object>> getAllFeatures() { Map<String, Object> response = new HashMap<>(); response.put("features", featureFlagService.getAllFeatures()); response.put("lastUpdate", Instant.now()); return ResponseEntity.ok(response); } @GetMapping("/{featureName}") public ResponseEntity<Map<String, Object>> getFeature(@PathVariable String featureName) { Map<String, Object> details = featureFlagService.getFeatureDetails(featureName); if (details.isEmpty()) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(details); } @PostMapping("/{featureName}/refresh") public ResponseEntity<String> refreshFeature(@PathVariable String featureName) { featureFlagService.refreshToggles(); return ResponseEntity.ok("Feature flags refreshed"); } @GetMapping("/context") public ResponseEntity<UnleashContext> getCurrentContext() { UnleashContext context = featureFlagService.getContextProvider().getContext(); return ResponseEntity.ok(context); } @PostMapping("/context") public ResponseEntity<String> setContext(@RequestBody Map<String, String> contextProps) { UnleashContext context = UnleashContext.builder() .properties(contextProps) .build(); featureFlagService.getContextProvider().setContext(context); return ResponseEntity.ok("Context updated"); } } Testing
1. Unit Tests
@ExtendWith(MockitoExtension.class) class FeatureFlagServiceTest { @Mock private Unleash unleash; @Mock private UnleashContextProvider contextProvider; @InjectMocks private FeatureFlagService featureFlagService; @Test void shouldReturnTrueWhenFeatureIsEnabled() { // Given String featureName = "new-feature"; when(unleash.isEnabled(featureName)).thenReturn(true); // When boolean result = featureFlagService.isEnabled(featureName); // Then assertThat(result).isTrue(); verify(unleash).isEnabled(featureName); } @Test void shouldReturnVariantForUser() { // Given String featureName = "ab-test"; String userId = "user123"; Variant expectedVariant = new Variant("variant-a", true, null); when(unleash.getVariant(eq(featureName), any(UnleashContext.class))) .thenReturn(expectedVariant); // When Variant result = featureFlagService.getVariant(featureName, userId); // Then assertThat(result).isEqualTo(expectedVariant); } } @SpringBootTest class FeatureFlagIntegrationTest { @Autowired private FeatureFlagService featureFlagService; @Autowired private PaymentService paymentService; @Test void shouldUseFallbackWhenFeatureDisabled() { // Given - feature is disabled by default in test // When AnalyticsData result = paymentService.getAdvancedAnalytics("test-user"); // Then - should use basic analytics fallback assertThat(result).isNotNull(); } @Test void shouldInjectCorrectBeansBasedOnFeatureFlags() { // This test would require setting up specific feature flag states // before application context initialization } } 2. Test Configuration
@TestConfiguration public class TestUnleashConfig { @Bean @Primary public Unleash testUnleash() { return new MockUnleash(); } public static class MockUnleash implements Unleash { private final Map<String, Boolean> features = new HashMap<>(); private final Map<String, Variant> variants = new HashMap<>(); public void enable(String featureName) { features.put(featureName, true); } public void disable(String featureName) { features.put(featureName, false); } public void setVariant(String featureName, Variant variant) { variants.put(featureName, variant); } @Override public boolean isEnabled(String toggleName) { return features.getOrDefault(toggleName, false); } @Override public boolean isEnabled(String toggleName, boolean defaultSetting) { return features.getOrDefault(toggleName, defaultSetting); } @Override public boolean isEnabled(String toggleName, UnleashContext context) { return isEnabled(toggleName); } @Override public boolean isEnabled(String toggleName, UnleashContext context, boolean defaultSetting) { return isEnabled(toggleName, defaultSetting); } @Override public Variant getVariant(String toggleName, UnleashContext context) { return variants.getOrDefault(toggleName, new Variant("disabled", false, null)); } @Override public Variant getVariant(String toggleName, UnleashContext context, Variant defaultValue) { return variants.getOrDefault(toggleName, defaultValue); } @Override public MoreOperations more() { return new MoreOperations() { @Override public void count(String toggleName, boolean enabled) {} @Override public void countVariant(String toggleName, String variantName) {} @Override public List<String> getFeatureToggleNames() { return new ArrayList<>(features.keySet()); } @Override public void fetchToggles() {} }; } } } Monitoring and Metrics
1. Metrics Export
@Component @Slf4j public class FeatureFlagMetrics { private final FeatureFlagService featureFlagService; private final MeterRegistry meterRegistry; private final Map<String, Gauge> featureGauges = new HashMap<>(); public FeatureFlagMetrics(FeatureFlagService featureFlagService, MeterRegistry meterRegistry) { this.featureFlagService = featureFlagService; this.meterRegistry = meterRegistry; initializeMetrics(); } @Scheduled(fixedRate = 30000) // Every 30 seconds public void updateFeatureMetrics() { Map<String, Boolean> features = featureFlagService.getAllFeatures(); features.forEach((featureName, enabled) -> { Gauge gauge = featureGauges.computeIfAbsent(featureName, name -> Gauge.builder("feature.flag.status") .tag("feature", name) .description("Status of feature flag") .register(meterRegistry) ); // Update gauge value (1 for enabled, 0 for disabled) gauge.set(enabled ? 1 : 0); }); } private void initializeMetrics() { // Initialize counters for feature usage Counter.builder("feature.flag.checked") .description("Number of times feature flags were checked") .register(meterRegistry); } } @Aspect @Component @Slf4j public class FeatureFlagMetricsAspect { private final Counter featureCheckCounter; public FeatureFlagMetricsAspect(MeterRegistry meterRegistry) { this.featureCheckCounter = Counter.builder("feature.flag.checked") .description("Number of feature flag checks") .register(meterRegistry); } @Around("execution(* com.example.service.FeatureFlagService.isEnabled(..))") public Object countFeatureChecks(ProceedingJoinPoint joinPoint) throws Throwable { featureCheckCounter.increment(); return joinPoint.proceed(); } } Best Practices
1. Naming Conventions
@Service @Slf4j public class FeatureFlagNaming { // Use consistent naming patterns private static final String FEATURE_PREFIX = "ff-"; private static final String EXPERIMENT_PREFIX = "ab-"; private static final String RELEASE_PREFIX = "release-"; public static String featureName(String service, String feature) { return FEATURE_PREFIX + service + "-" + feature; } public static String experimentName(String area, String experiment) { return EXPERIMENT_PREFIX + area + "-" + experiment; } public static String releaseName(String service, String version) { return RELEASE_PREFIX + service + "-" + version; } } 2. Cleanup and Maintenance
@Service @Slf4j public class FeatureFlagCleanupService { private final FeatureFlagService featureFlagService; private final Set<String> usedFeatures = ConcurrentHashMap.newKeySet(); public FeatureFlagCleanupService(FeatureFlagService featureFlagService) { this.featureFlagService = featureFlagService; } public void trackFeatureUsage(String featureName) { usedFeatures.add(featureName); } @Scheduled(cron = "0 0 1 * * ?") // Daily at 1 AM public void reportUnusedFeatures() { Map<String, Boolean> allFeatures = featureFlagService.getAllFeatures(); Set<String> unusedFeatures = new HashSet<>(allFeatures.keySet()); unusedFeatures.removeAll(usedFeatures); if (!unusedFeatures.isEmpty()) { log.warn("Potentially unused feature flags: {}", unusedFeatures); // Could send notification to team } // Reset usage tracking for new period usedFeatures.clear(); } } 3. Configuration
# application.yml unleash: app-name: "user-service" environment: "${SPRING_PROFILES_ACTIVE:development}" api-url: "http://localhost:4242/api" api-token: "${UNLEASH_API_TOKEN:default:development.unleash-insecure-api-token}" fetch-toggles-interval: 10 send-metrics-interval: 30 synchronous-fetch-on-initialisation: true backup-file: true management: endpoints: web: exposure: include: health,metrics,info,features endpoint: features: enabled: true logging: level: io.getunleash: DEBUG Conclusion
Implementing feature flags with Unleash in Java provides:
- Safe deployments with the ability to quickly disable problematic features
- Gradual rollouts and canary releases
- A/B testing capabilities for data-driven decisions
- Runtime configuration without code changes
- User-specific feature targeting
The integration with Spring Boot makes it easy to incorporate feature flags throughout your application, from simple boolean toggles to complex multivariate experiments. The architecture supports both technical and product feature flags, enabling collaboration between development and business teams.
By following the patterns shown above, you can build a robust feature flagging system that improves deployment safety, enables experimentation, and provides fine-grained control over feature releases.