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.