google-site-verification: google61fe8ba583a51912.html
Dynamic Feature Control: Implementing Feature Flags with Unleash in Java

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

  1. Access Unleash UI at http://localhost:4242
  2. Default credentials: admin / unleash4all
  3. 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

  1. Naming Conventions: Use consistent, descriptive names for feature flags
  2. Cleanup Strategy: Regularly review and remove unused flags
  3. Fallback Behavior: Always have sensible defaults when Unleash is unavailable
  4. Monitoring: Track flag usage and performance impact
  5. Documentation: Document each flag's purpose and expected lifecycle
  6. Security: Use appropriate API tokens and restrict access to Unleash dashboard
  7. 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.

Leave a Reply

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


Macro Nepal Helper