Dynamic Feature Management: ConfigCat Integration in Java Applications

Article

ConfigCat is a feature flag and remote configuration service that allows Java developers to deploy features safely, perform A/B testing, and control application behavior without code deployments. This guide covers everything from basic setup to advanced patterns for Java applications.


What is ConfigCat?

ConfigCat is a feature management service that enables you to separate feature releases from code deployments. You can turn features ON/OFF, target specific user segments, and roll out features gradually—all through a web dashboard without touching your production code.

Key Benefits for Java Teams:

  • Safe Deployments: Release features with kill switches
  • Gradual Rollouts: Canary releases and percentage-based rollouts
  • User Targeting: Enable features for specific users, groups, or regions
  • A/B Testing: Experiment with different feature variations
  • No Downtime: Change features instantly without deployments

Installation and Setup

1. Add ConfigCat Dependency

Maven:

<dependencies> <dependency> <groupId>com.configcat</groupId> <artifactId>configcat-client-java</artifactId> <version>9.0.0</version> </dependency> </dependencies>

Gradle:

dependencies { implementation 'com.configcat:configcat-client-java:9.0.0' }

2. Get Your SDK Key

  1. Sign up at ConfigCat
  2. Create a new feature flag
  3. Copy your SDK Key from the dashboard

Basic Configuration

1. Singleton Client Setup

package com.example.configcat; import com.configcat.ConfigCatClient; public class ConfigCatFactory { private static final String SDK_KEY = "YOUR_SDK_KEY_HERE"; private static ConfigCatClient client; public static synchronized ConfigCatClient getClient() { if (client == null) { client = ConfigCatClient.get(SDK_KEY); } return client; } public static void shutdown() { if (client != null) { client.close(); client = null; } } }

2. Environment-Based Configuration

package com.example.configcat; import com.configcat.ConfigCatClient; import com.configcat.PollingModes; public class ConfigCatManager { private static ConfigCatClient client; public static ConfigCatClient getClient() { if (client == null) { String sdkKey = System.getenv("CONFIGCAT_SDK_KEY"); if (sdkKey == null || sdkKey.trim().isEmpty()) { throw new IllegalStateException("CONFIGCAT_SDK_KEY environment variable is required"); } client = ConfigCatClient.newBuilder() .mode(PollingModes.autoPoll(60)) // Check for updates every 60 seconds .build(sdkKey); } return client; } }

Basic Feature Flag Usage

1. Simple Feature Toggle

package com.example.service; import com.configcat.ConfigCatClient; import com.example.configcat.ConfigCatFactory; public class FeatureService { private final ConfigCatClient configCatClient; public FeatureService() { this.configCatClient = ConfigCatFactory.getClient(); } public boolean isNewSearchEnabled() { return configCatClient.getValue(Boolean.class, "newSearchEnabled", false); } public boolean isPremiumFeaturesEnabled() { return configCatClient.getValue(Boolean.class, "premiumFeatures", false); } public String getUiTheme() { return configCatClient.getValue(String.class, "uiTheme", "light"); } public int getMaxSearchResults() { return configCatClient.getValue(Integer.class, "maxSearchResults", 10); } }

2. Usage in Spring Boot Controller

package com.example.controller; import com.example.service.FeatureService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") public class SearchController { private final FeatureService featureService; public SearchController(FeatureService featureService) { this.featureService = featureService; } @GetMapping("/search") public ResponseEntity<?> search(@RequestParam String query) { if (featureService.isNewSearchEnabled()) { // New search algorithm return ResponseEntity.ok(performNewSearch(query)); } else { // Old search algorithm return ResponseEntity.ok(performLegacySearch(query)); } } @GetMapping("/premium") public ResponseEntity<?> getPremiumFeatures() { if (!featureService.isPremiumFeaturesEnabled()) { return ResponseEntity.status(403).body("Premium features are not available"); } return ResponseEntity.ok(getPremiumContent()); } private Object performNewSearch(String query) { // Implement new search logic return Map.of( "results", "from new search", "query", query, "algorithm", "v2" ); } private Object performLegacySearch(String query) { // Implement legacy search logic return Map.of( "results", "from legacy search", "query", query, "algorithm", "v1" ); } private Object getPremiumContent() { return Map.of( "premium", true, "exclusiveContent", "Premium data here" ); } }

User-Based Targeting

1. User Object Creation

package com.example.model; import com.configcat.User; public class ConfigCatUser { public static User fromUserEntity(UserEntity user) { User configCatUser = new User(user.getId()); // Set standard attributes configCatUser.setEmail(user.getEmail()); configCatUser.setCountry(user.getCountry()); configCatUser.setIp(user.getLastLoginIp()); // Set custom attributes configCatUser.setCustom("subscriptionPlan", user.getSubscriptionPlan()); configCatUser.setCustom("registrationDate", user.getRegistrationDate().toString()); configCatUser.setCustom("totalPurchases", String.valueOf(user.getTotalPurchases())); configCatUser.setCustom("userRole", user.getRole()); configCatUser.setCustom("isTrialUser", String.valueOf(user.isTrialUser())); return configCatUser; } public static User fromRequest(String userId, String email, String country, String subscriptionPlan, String userRole) { User user = new User(userId); user.setEmail(email); user.setCountry(country); user.setCustom("subscriptionPlan", subscriptionPlan); user.setCustom("userRole", userRole); return user; } }

2. User-Specific Feature Flags

package com.example.service; import com.configcat.ConfigCatClient; import com.configcat.User; import com.example.configcat.ConfigCatFactory; import com.example.model.ConfigCatUser; import com.example.model.UserEntity; import org.springframework.stereotype.Service; import java.util.Map; @Service public class UserFeatureService { private final ConfigCatClient configCatClient; public UserFeatureService() { this.configCatClient = ConfigCatFactory.getClient(); } public boolean isDarkModeEnabled(UserEntity user) { User configCatUser = ConfigCatUser.fromUserEntity(user); return configCatClient.getValue(Boolean.class, "darkModeEnabled", configCatUser, false); } public boolean isBetaFeatureEnabled(UserEntity user, String featureKey) { User configCatUser = ConfigCatUser.fromUserEntity(user); return configCatClient.getValue(Boolean.class, featureKey, configCatUser, false); } public String getPersonalizedWelcomeMessage(UserEntity user) { User configCatUser = ConfigCatUser.fromUserEntity(user); return configCatClient.getValue(String.class, "welcomeMessage", configCatUser, "Welcome!"); } public double getDiscountPercentage(UserEntity user) { User configCatUser = ConfigCatUser.fromUserEntity(user); return configCatClient.getValue(Double.class, "discountPercentage", configCatUser, 0.0); } public boolean canAccessPremiumContent(UserEntity user) { User configCatUser = ConfigCatUser.fromUserEntity(user); // Check multiple conditions boolean hasPremiumAccess = configCatClient.getValue( Boolean.class, "premiumContentAccess", configCatUser, false); boolean isNotTrial = !"trial".equals(user.getSubscriptionPlan()); return hasPremiumAccess && isNotTrial; } }

3. Spring Controller with User Context

package com.example.controller; import com.example.model.UserEntity; import com.example.service.UserFeatureService; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/user") public class UserFeatureController { private final UserFeatureService userFeatureService; public UserFeatureController(UserFeatureService userFeatureService) { this.userFeatureService = userFeatureService; } @GetMapping("/features") public ResponseEntity<Map<String, Object>> getUserFeatures(@AuthenticationPrincipal UserEntity user) { Map<String, Object> features = new HashMap<>(); features.put("darkMode", userFeatureService.isDarkModeEnabled(user)); features.put("betaAccess", userFeatureService.isBetaFeatureEnabled(user, "betaProgram")); features.put("welcomeMessage", userFeatureService.getPersonalizedWelcomeMessage(user)); features.put("discountPercentage", userFeatureService.getDiscountPercentage(user)); features.put("premiumContent", userFeatureService.canAccessPremiumContent(user)); return ResponseEntity.ok(features); } @GetMapping("/profile") public ResponseEntity<?> getUserProfile(@AuthenticationPrincipal UserEntity user) { Map<String, Object> profile = new HashMap<>(); profile.put("user", user); profile.put("features", getUserFeatures(user).getBody()); // Apply feature-dependent UI elements if (userFeatureService.isDarkModeEnabled(user)) { profile.put("uiTheme", "dark"); } else { profile.put("uiTheme", "light"); } return ResponseEntity.ok(profile); } }

Advanced Configuration

1. Custom Polling Configuration

package com.example.configcat; import com.configcat.ConfigCatClient; import com.configcat.PollingModes; import com.configcat.ConfigCatLogger; import com.configcat.LogLevel; public class AdvancedConfigCatManager { private static ConfigCatClient client; public static ConfigCatClient getClient() { if (client == null) { String sdkKey = System.getenv("CONFIGCAT_SDK_KEY"); client = ConfigCatClient.newBuilder() .mode(PollingModes.autoPoll( intervalInSeconds: 30, // Check every 30 seconds maxInitWaitTimeSeconds: 5 // Wait up to 5 seconds for first configuration )) .logger(new ConfigCatLogger() { @Override public void log(String message, Throwable throwable, LogLevel level) { // Integrate with your logging framework System.out.println("[" + level + "] " + message); if (throwable != null) { throwable.printStackTrace(); } } }) .connectTimeoutSeconds(10) .readTimeoutSeconds(10) .build(sdkKey); } return client; } // Lazy load mode for serverless environments public static ConfigCatClient getLazyClient() { if (client == null) { String sdkKey = System.getenv("CONFIGCAT_SDK_KEY"); client = ConfigCatClient.newBuilder() .mode(PollingModes.lazyLoad( cacheTimeToLiveSeconds: 60 // Cache for 60 seconds )) .build(sdkKey); } return client; } // Manual polling for maximum control public static ConfigCatClient getManualClient() { if (client == null) { String sdkKey = System.getenv("CONFIGCAT_SDK_KEY"); client = ConfigCatClient.newBuilder() .mode(PollingModes.manualPoll()) .build(sdkKey); } return client; } }

2. Spring Boot Configuration

package com.example.config; import com.configcat.ConfigCatClient; import com.configcat.PollingModes; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ConfigCatConfig { @Value("${configcat.sdk-key:}") private String sdkKey; @Bean public ConfigCatClient configCatClient() { if (sdkKey == null || sdkKey.trim().isEmpty()) { throw new IllegalStateException("configcat.sdk-key property is required"); } return ConfigCatClient.newBuilder() .mode(PollingModes.autoPoll(60)) .build(sdkKey); } }

application.yml:

configcat: sdk-key: ${CONFIGCAT_SDK_KEY:} # Optional: Feature flag defaults feature-flags: defaults: new-search-enabled: false premium-features: false ui-theme: "light" max-results: 50

A/B Testing and Experimentation

1. Feature Variation Testing

package com.example.service; import com.configcat.ConfigCatClient; import com.configcat.User; import org.springframework.stereotype.Service; import java.util.Map; @Service public class ExperimentService { private final ConfigCatClient configCatClient; public ExperimentService(ConfigCatClient configCatClient) { this.configCatClient = configCatClient; } public String getButtonColor(User user) { return configCatClient.getValue(String.class, "buttonColorExperiment", user, "blue"); } public String getPricingTier(User user) { return configCatClient.getValue(String.class, "pricingExperiment", user, "basic"); } public Map<String, Object> getOnboardingFlow(User user) { String flowVersion = configCatClient.getValue(String.class, "onboardingFlow", user, "v1"); return switch (flowVersion) { case "v2" -> Map.of( "version", "v2", "steps", 5, "showTutorial", true, "skipOption", false ); case "v3" -> Map.of( "version", "v3", "steps", 3, "showTutorial", false, "skipOption", true ); default -> Map.of( "version", "v1", "steps", 4, "showTutorial", true, "skipOption", false ); }; } public boolean isInExperimentalGroup(User user, String experiment) { String variation = configCatClient.getValue(String.class, experiment, user, "control"); return !"control".equals(variation); } }

2. Experiment Controller

package com.example.controller; import com.configcat.User; import com.example.model.UserEntity; import com.example.service.ExperimentService; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/experiment") public class ExperimentController { private final ExperimentService experimentService; public ExperimentController(ExperimentService experimentService) { this.experimentService = experimentService; } @GetMapping("/ui-settings") public ResponseEntity<Map<String, Object>> getUiSettings(@AuthenticationPrincipal UserEntity user) { User configCatUser = convertToConfigCatUser(user); Map<String, Object> settings = Map.of( "buttonColor", experimentService.getButtonColor(configCatUser), "onboardingFlow", experimentService.getOnboardingFlow(configCatUser), "inPricingExperiment", experimentService.isInExperimentalGroup(configCatUser, "pricingExperiment") ); return ResponseEntity.ok(settings); } @PostMapping("/track-conversion") public ResponseEntity<?> trackConversion(@AuthenticationPrincipal UserEntity user, @RequestParam String experiment, @RequestParam String action) { User configCatUser = convertToConfigCatUser(user); // Track conversion for A/B testing // This would typically go to your analytics platform System.out.printf("Conversion tracked: user=%s, experiment=%s, action=%s%n", user.getId(), experiment, action); return ResponseEntity.ok().build(); } private User convertToConfigCatUser(UserEntity user) { return new User(user.getId()) .setEmail(user.getEmail()) .setCountry(user.getCountry()) .setCustom("subscriptionPlan", user.getSubscriptionPlan()); } }

Caching and Performance Optimization

1. Cached Feature Service

package com.example.service; import com.configcat.ConfigCatClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service public class CachedFeatureService { private final ConfigCatClient configCatClient; private final Map<String, Boolean> booleanCache = new ConcurrentHashMap<>(); private final Map<String, String> stringCache = new ConcurrentHashMap<>(); private final Map<String, Integer> intCache = new ConcurrentHashMap<>(); public CachedFeatureService(ConfigCatClient configCatClient) { this.configCatClient = configCatClient; } public boolean isFeatureEnabled(String key) { return booleanCache.computeIfAbsent(key, k -> configCatClient.getValue(Boolean.class, k, false)); } public String getStringValue(String key, String defaultValue) { return stringCache.computeIfAbsent(key, k -> configCatClient.getValue(String.class, k, defaultValue)); } public int getIntValue(String key, int defaultValue) { return intCache.computeIfAbsent(key, k -> configCatClient.getValue(Integer.class, k, defaultValue)); } @Scheduled(fixedRate = 60000) // Clear cache every minute public void clearCache() { booleanCache.clear(); stringCache.clear(); intCache.clear(); } // Manual cache invalidation public void invalidateCache() { clearCache(); } public void invalidateKey(String key) { booleanCache.remove(key); stringCache.remove(key); intCache.remove(key); } }

Error Handling and Fallbacks

1. Resilient Feature Service

package com.example.service; import com.configcat.ConfigCatClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service public class ResilientFeatureService { private static final Logger logger = LoggerFactory.getLogger(ResilientFeatureService.class); private final ConfigCatClient configCatClient; private final Map<String, Object> localCache = new ConcurrentHashMap<>(); public ResilientFeatureService(ConfigCatClient configCatClient) { this.configCatClient = configCatClient; } public boolean getFeatureFlagWithFallback(String key, boolean fallback) { try { boolean value = configCatClient.getValue(Boolean.class, key, fallback); localCache.put(key, value); return value; } catch (Exception e) { logger.warn("Failed to get feature flag '{}', using fallback: {}", key, fallback, e); return (boolean) localCache.getOrDefault(key, fallback); } } public String getStringWithFallback(String key, String fallback) { try { String value = configCatClient.getValue(String.class, key, fallback); localCache.put(key, value); return value; } catch (Exception e) { logger.warn("Failed to get string value '{}', using fallback: {}", key, fallback, e); return (String) localCache.getOrDefault(key, fallback); } } public void refreshCache() { // Force refresh of all cached values localCache.keySet().forEach(key -> { try { Object value = configCatClient.getValue(Object.class, key, localCache.get(key)); localCache.put(key, value); } catch (Exception e) { logger.warn("Failed to refresh key '{}'", key, e); } }); } }

Testing with ConfigCat

1. Unit Test with Mocked ConfigCat

package com.example.service; import com.configcat.ConfigCatClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class FeatureServiceTest { @Mock private ConfigCatClient configCatClient; private FeatureService featureService; @BeforeEach void setUp() { featureService = new FeatureService(configCatClient); } @Test void testNewSearchEnabled() { when(configCatClient.getValue(eq(Boolean.class), eq("newSearchEnabled"), anyBoolean())) .thenReturn(true); assertTrue(featureService.isNewSearchEnabled()); } @Test void testNewSearchDisabled() { when(configCatClient.getValue(eq(Boolean.class), eq("newSearchEnabled"), anyBoolean())) .thenReturn(false); assertFalse(featureService.isNewSearchEnabled()); } @Test void testFallbackValue() { when(configCatClient.getValue(eq(Boolean.class), eq("nonExistentFlag"), eq(false))) .thenReturn(false); // This would use the fallback value assertFalse(featureService.isNewSearchEnabled()); } }

2. Integration Test

package com.example.integration; import com.configcat.ConfigCatClient; import com.example.service.FeatureService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @TestPropertySource(properties = { "configcat.sdk-key=TEST_SDK_KEY" }) class FeatureServiceIntegrationTest { @Autowired private FeatureService featureService; @Autowired private ConfigCatClient configCatClient; @Test void testFeatureServiceInitialization() { assertNotNull(featureService); assertNotNull(configCatClient); } }

Best Practices

1. Environment-Specific Configuration

public class EnvironmentAwareConfigCat { private static String getSdkKey() { String environment = System.getenv("APP_ENVIRONMENT"); return switch (environment) { case "production" -> System.getenv("CONFIGCAT_PROD_SDK_KEY"); case "staging" -> System.getenv("CONFIGCAT_STAGING_SDK_KEY"); case "development" -> System.getenv("CONFIGCAT_DEV_SDK_KEY"); default -> "localhost"; // For local development }; } }

2. Feature Flag Naming Convention

public class FeatureFlagNames { // Feature toggles public static final String NEW_SEARCH_ALGORITHM = "new-search-algorithm"; public static final String PREMIUM_FEATURES = "premium-features"; public static final String DARK_MODE = "dark-mode-enabled"; // Experiment flags public static final String BUTTON_COLOR_EXPERIMENT = "experiment-button-color"; public static final String ONBOARDING_FLOW_EXPERIMENT = "experiment-onboarding-flow"; // Operational flags public static final String MAINTENANCE_MODE = "maintenance-mode"; public static final String READ_ONLY_MODE = "read-only-mode"; // Release flags public static final String NEW_UI_RELEASE = "release-new-ui"; public static final String API_V2_RELEASE = "release-api-v2"; }

Conclusion

ConfigCat provides Java developers with a powerful feature management solution that enables:

  • Safe Feature Releases: Deploy code with features disabled, then enable via ConfigCat
  • Gradual Rollouts: Control feature visibility with percentage rollouts
  • Targeted Releases: Enable features for specific users, regions, or custom attributes
  • A/B Testing: Run experiments with different feature variations
  • Operational Control: Implement kill switches and maintenance modes

By integrating ConfigCat into your Java applications, you gain fine-grained control over feature releases while maintaining system stability and enabling data-driven development decisions.

Leave a Reply

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


Macro Nepal Helper