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