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
- Sign up at ConfigCat
- Create a new feature flag
- 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.