Article
Feature flags (or feature toggles) have become an essential tool for modern software development, enabling teams to release features safely, conduct A/B tests, and manage production systems dynamically. Unleash is a popular open-source feature flag service that provides a robust, scalable solution for managing feature flags across your applications.
In this guide, we'll explore how to integrate Unleash with Java applications to implement powerful feature flagging strategies.
Why Use Unleash for Feature Flags?
- Gradual Rollouts: Safely release features to percentage-based user segments
- Targeted Releases: Enable features for specific users, groups, or environments
- Kill Switches: Instantly disable problematic features without redeployment
- A/B Testing: Conduct experiments with built-in analytics
- No Restart Required: Change feature availability without application restarts
- Open Source: Self-hosted option with enterprise features available
Part 1: Setting Up Unleash
1.1 Running Unleash
Option A: Docker Compose (Recommended for Development)
# docker-compose.yml version: '3' services: postgres: image: postgres:13 environment: POSTGRES_PASSWORD: password POSTGRES_USER: unleash POSTGRES_DB: unleash volumes: - postgres_data:/var/lib/postgresql/data unleash: image: unleashorg/unleash-server:latest environment: DATABASE_URL: postgres://unleash:password@postgres:5432/unleash DATABASE_SSL: "false" ports: - "4242:4242" depends_on: - postgres volumes: postgres_data:
Run with: docker-compose up -d
Option B: Unleash Hosted Service
Sign up at Unleash Hosted for a managed solution.
1.2 Initial Setup
- Access Unleash UI at
http://localhost:4242 - Default credentials:
admin / unleash4all - Create your first feature flag:
- Name:
newPaymentService - Description: "Enable new payment processing service"
- Toggle type: Release
Part 2: Java Client Integration
2.1 Dependencies Setup
Maven:
<dependencies> <dependency> <groupId>io.getunleash</groupId> <artifactId>unleash-client-java</artifactId> <version>9.2.0</version> </dependency> <!-- Spring Boot Starter (Optional) --> <dependency> <groupId>io.getunleash</groupId> <artifactId>unleash-client-spring-boot-starter</artifactId> <version>9.2.0</version> </dependency> <!-- For metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> </dependencies>
Gradle:
implementation 'io.getunleash:unleash-client-java:9.2.0' implementation 'io.getunleash:unleash-client-spring-boot-starter:9.2.0'
2.2 Basic Configuration
Programmatic Configuration:
// File: src/main/java/com/example/config/UnleashConfig.java
@Configuration
public class UnleashConfig {
@Value("${app.environment:development}")
private String environment;
@Bean
public Unleash unleash() {
return DefaultUnleash.builder()
.unleashConfig(io.getunleash.UnleashConfig.builder()
.appName("my-java-app")
.instanceId(environment + "-instance-1")
.unleashAPI("http://localhost:4242/api/")
.apiKey("default:development.unleash-insecure-api-token") // For v4.x+
.fetchTogglesInterval(10) // Seconds
.sendMetricsInterval(60) // Seconds
.build())
.build();
}
}
Spring Boot Auto-Configuration:
# application.yml
unleash:
app-name: my-java-app
instance-id: ${spring.application.instance-id:instance-1}
api-url: http://localhost:4242/api/
api-key: default:development.unleash-insecure-api-token
fetch-toggles-interval: 10
send-metrics-interval: 60
synchronous-fetch-on-initialisation: true
Part 3: Implementing Feature Flags in Code
3.1 Basic Feature Flag Usage
// File: src/main/java/com/example/service/PaymentService.java
@Service
public class PaymentService {
private final Unleash unleash;
private final OldPaymentProcessor oldProcessor;
private final NewPaymentProcessor newProcessor;
public PaymentService(Unleash unleash, OldPaymentProcessor oldProcessor,
NewPaymentProcessor newProcessor) {
this.unleash = unleash;
this.oldProcessor = oldProcessor;
this.newProcessor = newProcessor;
}
public PaymentResult processPayment(PaymentRequest request) {
if (unleash.isEnabled("newPaymentService")) {
return newProcessor.process(request);
} else {
return oldProcessor.process(request);
}
}
public PaymentResult processPaymentWithContext(PaymentRequest request, User user) {
UnleashContext context = UnleashContext.builder()
.userId(user.getId())
.sessionId(user.getSessionId())
.remoteAddress(user.getIpAddress())
.addProperty("tier", user.getPlanTier())
.addProperty("country", user.getCountry())
.build();
if (unleash.isEnabled("newPaymentService", context)) {
return newProcessor.process(request);
} else {
return oldProcessor.process(request);
}
}
}
3.2 Advanced Flag Strategies
Gradual Rollout:
@Service
public class UserOnboardingService {
private final Unleash unleash;
public UserOnboardingService(Unleash unleash) {
this.unleash = unleash;
}
public boolean shouldShowNewOnboarding(User user) {
UnleashContext context = UnleashContext.builder()
.userId(user.getId())
.addProperty("tier", user.getPlanTier())
.addProperty("signupDate", user.getSignupDate().toString())
.build();
return unleash.isEnabled("newOnboardingExperience", context);
}
public boolean isInBetaGroup(User user) {
UnleashContext context = UnleashContext.builder()
.userId(user.getId())
.addProperty("email", user.getEmail())
.build();
return unleash.isEnabled("betaFeatures", context);
}
}
A/B Testing Implementation:
// File: src/main/java/com/example/service/RecommendationService.java
@Service
public class RecommendationService {
private final Unleash unleash;
private final Map<String, RecommendationStrategy> strategies;
public RecommendationService(Unleash unleash, List<RecommendationStrategy> strategyList) {
this.unleash = unleash;
this.strategies = strategyList.stream()
.collect(Collectors.toMap(RecommendationStrategy::getVariant, Function.identity()));
}
public List<Product> getRecommendations(User user, String category) {
UnleashContext context = UnleashContext.builder()
.userId(user.getId())
.addProperty("category", category)
.build();
// Get variant for A/B testing
Variant variant = unleash.getVariant("recommendationAlgorithm", context);
String variantKey = variant.getName();
RecommendationStrategy strategy = strategies.getOrDefault(variantKey, strategies.get("control"));
return strategy.getRecommendations(user, category);
}
public void trackRecommendationPerformance(User user, String variant, double conversionRate) {
// Track metrics for A/B test analysis
metricsService.trackVariantPerformance("recommendationAlgorithm", variant, conversionRate);
}
}
3.3 Custom Activation Strategies
// File: src/main/java/com/example/unleash/PlanTierStrategy.java
@Component
public class PlanTierStrategy implements ActivationStrategy {
private static final String STRATEGY_NAME = "PlanTierStrategy";
private static final String PARAM_TIERS = "allowedTiers";
@Override
public String getName() {
return STRATEGY_NAME;
}
@Override
public boolean isEnabled(Map<String, String> parameters) {
// Default to enabled if no parameters
return true;
}
@Override
public boolean isEnabled(Map<String, String> parameters, UnleashContext context) {
String userTier = context.getProperties().get("tier");
String allowedTiers = parameters.get(PARAM_TIERS);
if (userTier == null || allowedTiers == null) {
return false;
}
List<String> allowedTierList = Arrays.asList(allowedTiers.split(",\\s*"));
return allowedTierList.contains(userTier);
}
}
Part 4: Spring Boot Integration
4.1 Configuration Properties
// File: src/main/java/com/example/config/UnleashProperties.java
@ConfigurationProperties(prefix = "unleash")
@Data
public class UnleashProperties {
private String appName;
private String instanceId;
private String apiUrl;
private String apiKey;
private int fetchTogglesInterval = 10;
private int sendMetricsInterval = 60;
private boolean synchronousFetchOnInitialisation = true;
private String environment;
}
4.2 Conditional Configuration with Feature Flags
// File: src/main/java/com/example/config/FeatureFlagConfiguration.java
@Configuration
public class FeatureFlagConfiguration {
@Bean
@ConditionalOnFeatureToggle("newPaymentService")
public PaymentProcessor newPaymentProcessor() {
return new NewPaymentProcessor();
}
@Bean
@ConditionalOnFeatureToggle(value = "newPaymentService", havingValue = false)
public PaymentProcessor oldPaymentProcessor() {
return new OldPaymentProcessor();
}
@Bean
@ConditionalOnFeatureToggle("experimentalFeatures")
public ExperimentalService experimentalService() {
return new ExperimentalService();
}
}
// Custom Condition
public class ConditionalOnFeatureToggle implements Condition {
private final String toggleName;
private final boolean havingValue;
public ConditionalOnFeatureToggle(String toggleName) {
this(toggleName, true);
}
public ConditionalOnFeatureToggle(String toggleName, boolean havingValue) {
this.toggleName = toggleName;
this.havingValue = havingValue;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Unleash unleash = context.getBeanFactory().getBean(Unleash.class);
return unleash.isEnabled(toggleName) == havingValue;
}
}
4.3 Feature Flag Controller for Client-Side
// File: src/main/java/com/example/controller/FeatureFlagController.java
@RestController
@RequestMapping("/api/features")
public class FeatureFlagController {
private final Unleash unleash;
public FeatureFlagController(Unleash unleash) {
this.unleash = unleash;
}
@GetMapping
public Map<String, Boolean> getFeatureFlags(@RequestParam String userId,
@RequestParam(required = false) String sessionId) {
UnleashContext context = UnleashContext.builder()
.userId(userId)
.sessionId(sessionId)
.build();
return Map.of(
"newOnboarding", unleash.isEnabled("newOnboardingExperience", context),
"darkMode", unleash.isEnabled("darkMode", context),
"premiumFeatures", unleash.isEnabled("premiumFeatures", context)
);
}
@GetMapping("/variants")
public Map<String, String> getFeatureVariants(@RequestParam String userId) {
UnleashContext context = UnleashContext.builder().userId(userId).build();
return Map.of(
"recommendationAlgorithm", unleash.getVariant("recommendationAlgorithm", context).getName(),
"uiTheme", unleash.getVariant("uiTheme", context).getName()
);
}
}
Part 5: Testing Feature Flags
5.1 Unit Testing with Mocked Unleash
// File: src/test/java/com/example/service/PaymentServiceTest.java
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock
private Unleash unleash;
@Mock
private OldPaymentProcessor oldProcessor;
@Mock
private NewPaymentProcessor newProcessor;
private PaymentService paymentService;
@BeforeEach
void setUp() {
paymentService = new PaymentService(unleash, oldProcessor, newProcessor);
}
@Test
void shouldUseNewProcessorWhenFeatureEnabled() {
// Given
PaymentRequest request = new PaymentRequest("order-123", BigDecimal.valueOf(100));
PaymentResult expectedResult = new PaymentResult(true, "processed");
when(unleash.isEnabled("newPaymentService")).thenReturn(true);
when(newProcessor.process(request)).thenReturn(expectedResult);
// When
PaymentResult result = paymentService.processPayment(request);
// Then
assertThat(result.isSuccess()).isTrue();
verify(newProcessor).process(request);
verify(oldProcessor, never()).process(request);
}
@Test
void shouldUseOldProcessorWhenFeatureDisabled() {
// Given
PaymentRequest request = new PaymentRequest("order-123", BigDecimal.valueOf(100));
PaymentResult expectedResult = new PaymentResult(true, "processed");
when(unleash.isEnabled("newPaymentService")).thenReturn(false);
when(oldProcessor.process(request)).thenReturn(expectedResult);
// When
PaymentResult result = paymentService.processPayment(request);
// Then
assertThat(result.isSuccess()).isTrue();
verify(oldProcessor).process(request);
verify(newProcessor, never()).process(request);
}
}
5.2 Integration Testing
// File: src/test/java/com/example/service/FeatureFlagIntegrationTest.java
@SpringBootTest
@TestPropertySource(properties = {
"unleash.api-url=http://localhost:4242/api/",
"unleash.app-name=test-app"
})
class FeatureFlagIntegrationTest {
@Autowired
private Unleash unleash;
@Test
void shouldConnectToUnleashServer() {
assertThat(unleash).isNotNull();
// This will use whatever flags are configured in your Unleash instance
boolean isEnabled = unleash.isEnabled("someTestFlag");
// You might want to use a test-specific flag
assertThat(unleash.isEnabled("integrationTestFlag")).isFalse();
}
}
5.3 Testing with Fake Unleash
// File: src/test/java/com/example/service/FeatureToggleTest.java
class FeatureToggleTest {
private FakeUnleash unleash;
private PaymentService paymentService;
@BeforeEach
void setUp() {
unleash = new FakeUnleash();
paymentService = new PaymentService(unleash, mock(OldPaymentProcessor.class),
mock(NewPaymentProcessor.class));
}
@Test
void shouldRespectFeatureToggleState() {
// Enable the flag
unleash.enable("newPaymentService");
assertThat(unleash.isEnabled("newPaymentService")).isTrue();
// Disable the flag
unleash.disable("newPaymentService");
assertThat(unleash.isEnabled("newPaymentService")).isFalse();
// Enable multiple flags
unleash.enableAll();
assertThat(unleash.isEnabled("newPaymentService")).isTrue();
assertThat(unleash.isEnabled("otherFlag")).isTrue();
}
}
Part 6: Best Practices & Production Considerations
6.1 Monitoring and Metrics
// File: src/main/java/com/example/config/UnleashMetricsConfig.java
@Component
public class UnleashMetrics {
private final MeterRegistry meterRegistry;
private final Unleash unleash;
public UnleashMetrics(MeterRegistry meterRegistry, Unleash unleash) {
this.meterRegistry = meterRegistry;
this.unleash = unleash;
initializeMetrics();
}
private void initializeMetrics() {
Gauge.builder("unleash.toggles.count")
.description("Number of feature toggles")
.register(meterRegistry, unleash.more().getFeatureToggleNames().size());
}
public void trackToggleUsage(String toggleName, boolean enabled) {
Counter.builder("unleash.toggle.usage")
.tag("toggle", toggleName)
.tag("enabled", String.valueOf(enabled))
.register(meterRegistry)
.increment();
}
}
6.2 Circuit Breaker for Unleash
// File: src/main/java/com/example/config/UnleashCircuitBreaker.java
@Component
public class UnleashCircuitBreaker {
private final CircuitBreaker circuitBreaker;
private final Unleash unleash;
public UnleashCircuitBreaker(Unleash unleash, CircuitBreakerRegistry circuitBreakerRegistry) {
this.unleash = unleash;
this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("unleash");
}
public boolean isEnabled(String toggleName) {
return circuitBreaker.executeSupplier(() -> unleash.isEnabled(toggleName));
}
public boolean isEnabled(String toggleName, UnleashContext context) {
return circuitBreaker.executeSupplier(() -> unleash.isEnabled(toggleName, context));
}
}
6.3 Cleanup and Technical Debt Management
// File: src/main/java/com/example/service/FeatureFlagCleanupService.java
@Service
public class FeatureFlagCleanupService {
private final Unleash unleash;
private final Set<String> permanentFlags = Set.of("darkMode", "premiumFeatures");
@Scheduled(cron = "0 0 2 * * ?") // Daily at 2 AM
public void reportStaleFeatureFlags() {
Set<String> allToggles = unleash.more().getFeatureToggleNames();
Set<String> staleToggles = allToggles.stream()
.filter(toggle -> !permanentFlags.contains(toggle))
.filter(this::isRarelyUsed)
.collect(Collectors.toSet());
if (!staleToggles.isEmpty()) {
log.warn("Potential stale feature flags detected: {}", staleToggles);
// Send notification to team
}
}
private boolean isRarelyUsed(String toggleName) {
// Implement logic to check if flag is rarely changing or used
return true;
}
}
Best Practices Summary
- Naming Conventions: Use consistent, descriptive names for feature flags
- Cleanup Strategy: Regularly review and remove unused flags
- Fallback Behavior: Always have sensible defaults when Unleash is unavailable
- Monitoring: Track flag usage and performance impact
- Documentation: Document each flag's purpose and expected lifecycle
- Security: Use appropriate API tokens and restrict access to Unleash dashboard
- Testing: Test both enabled and disabled states for all feature flags
Conclusion
Implementing feature flags with Unleash in Java applications provides tremendous flexibility for safe deployments, gradual rollouts, and experimentation. By following the patterns outlined in this guide, you can build a robust feature flagging system that integrates seamlessly with your Spring Boot applications.
Remember that feature flags are temporary by nature - establish processes for regular cleanup to avoid technical debt. With proper implementation, Unleash can become a cornerstone of your continuous delivery pipeline, enabling faster, safer releases and data-driven product decisions.