Split.io Feature Flags Implementation in Java

Introduction

Split.io is a feature delivery platform that enables controlled rollouts, experimentation, and instant feature flag management. Implementing Split.io in Java applications allows teams to deploy features safely, perform A/B testing, and manage application behavior without code deployments.

Setup and Configuration

1. Maven Dependency

<dependency>
<groupId>io.split.client</groupId>
<artifactId>split-commons</artifactId>
<version>4.4.3</version>
</dependency>

2. Configuration Class

@Configuration
@ConfigurationProperties(prefix = "splitio")
@Data
public class SplitIOConfig {
private String apiKey;
private boolean enabled;
private long refreshRate = 30; // seconds
private String baseUrl = "https://sdk.split.io";
@Bean
@ConditionalOnProperty(name = "splitio.enabled", havingValue = "true")
public SplitClient splitClient() throws Exception {
SplitClientConfig config = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(10000)
.refreshRate(refreshRate)
.build();
SplitFactory splitFactory = SplitFactoryBuilder.build(apiKey, config);
SplitClient client = splitFactory.client();
client.blockUntilReady();
return client;
}
}

3. Application Properties

splitio:
api-key: ${SPLITIO_API_KEY:localhost}
enabled: true
refresh-rate: 30
base-url: https://sdk.split.io

Core Implementation

1. Service Layer

@Service
@Slf4j
public class FeatureFlagService {
private final SplitClient splitClient;
private final SplitManager splitManager;
private final SplitIOConfig config;
public FeatureFlagService(SplitClient splitClient, 
SplitManager splitManager,
SplitIOConfig config) {
this.splitClient = splitClient;
this.splitManager = splitManager;
this.config = config;
}
public boolean isFeatureEnabled(String featureFlag, String userId) {
return isFeatureEnabled(featureFlag, userId, null);
}
public boolean isFeatureEnabled(String featureFlag, String userId, 
Map<String, Object> attributes) {
if (!config.isEnabled()) {
log.warn("Split.io disabled, returning false for feature: {}", featureFlag);
return false;
}
try {
if (attributes != null && !attributes.isEmpty()) {
return splitClient.getTreatment(userId, featureFlag, attributes)
.equals("on");
} else {
return splitClient.getTreatment(userId, featureFlag)
.equals("on");
}
} catch (Exception e) {
log.error("Error checking feature flag: {}", featureFlag, e);
return false;
}
}
public String getFeatureTreatment(String featureFlag, String userId) {
return getFeatureTreatment(featureFlag, userId, null);
}
public String getFeatureTreatment(String featureFlag, String userId,
Map<String, Object> attributes) {
if (!config.isEnabled()) {
return "control";
}
try {
if (attributes != null && !attributes.isEmpty()) {
return splitClient.getTreatment(userId, featureFlag, attributes);
} else {
return splitClient.getTreatment(userId, featureFlag);
}
} catch (Exception e) {
log.error("Error getting treatment for feature: {}", featureFlag, e);
return "control";
}
}
}

2. Feature Flag Definitions

public class FeatureFlags {
public static final String NEW_USER_ONBOARDING = "new_user_onboarding_v2";
public static final String PAYMENT_PROCESSOR_V3 = "payment_processor_v3";
public static final String RECOMMENDATION_ENGINE = "ai_recommendation_engine";
public static final String DARK_MODE = "dark_mode_ui";
public static final String PREMIUM_FEATURES = "premium_features_beta";
// Treatment variations
public static final String TREATMENT_ON = "on";
public static final String TREATMENT_OFF = "off";
public static final String TREATMENT_CONTROL = "control";
public static final String TREATMENT_VARIANT_A = "variant_a";
public static final String TREATMENT_VARIANT_B = "variant_b";
}

Advanced Usage Patterns

1. Contextual Feature Flags with Attributes

@Service
public class UserService {
private final FeatureFlagService featureFlagService;
public void processUserRegistration(User user) {
Map<String, Object> attributes = new HashMap<>();
attributes.put("userTier", user.getTier());
attributes.put("registrationDate", user.getRegistrationDate());
attributes.put("country", user.getCountry());
attributes.put("deviceType", user.getDeviceType());
if (featureFlagService.isFeatureEnabled(
FeatureFlags.NEW_USER_ONBOARDING, user.getId(), attributes)) {
executeNewOnboardingFlow(user);
} else {
executeLegacyOnboardingFlow(user);
}
}
public boolean shouldShowPremiumFeatures(User user) {
Map<String, Object> attributes = Map.of(
"userTier", user.getTier(),
"accountAge", user.getAccountAgeInDays(),
"hasSubscription", user.hasActiveSubscription()
);
return featureFlagService.isFeatureEnabled(
FeatureFlags.PREMIUM_FEATURES, user.getId(), attributes);
}
}

2. A/B Testing Implementation

@Service
@Slf4j
public class ABTestingService {
private final FeatureFlagService featureFlagService;
private final AnalyticsService analyticsService;
public void trackFeatureExposure(String featureFlag, String userId, 
Map<String, Object> attributes) {
String treatment = featureFlagService.getFeatureTreatment(
featureFlag, userId, attributes);
analyticsService.trackEvent(userId, "feature_exposure", Map.of(
"feature_flag", featureFlag,
"treatment", treatment,
"timestamp", Instant.now()
));
}
public void processRecommendationRequest(User user, ContentRequest request) {
Map<String, Object> attributes = buildAttributes(user, request);
// Track exposure for A/B test
trackFeatureExposure(FeatureFlags.RECOMMENDATION_ENGINE, 
user.getId(), attributes);
String treatment = featureFlagService.getFeatureTreatment(
FeatureFlags.RECOMMENDATION_ENGINE, user.getId(), attributes);
List<Content> recommendations;
switch (treatment) {
case FeatureFlags.TREATMENT_VARIANT_A:
recommendations = getAIRecommendationsV1(user, request);
break;
case FeatureFlags.TREATMENT_VARIANT_B:
recommendations = getAIRecommendationsV2(user, request);
break;
default:
recommendations = getLegacyRecommendations(user, request);
}
// Track results for analysis
trackRecommendationResults(user.getId(), treatment, recommendations);
}
private Map<String, Object> buildAttributes(User user, ContentRequest request) {
Map<String, Object> attributes = new HashMap<>();
attributes.put("userTier", user.getTier());
attributes.put("userSegment", user.getSegment());
attributes.put("contentCategory", request.getCategory());
attributes.put("deviceType", request.getDeviceType());
attributes.put("timeOfDay", LocalTime.now().getHour());
return attributes;
}
}

3. Configuration-Based Feature Management

@Component
public class FeatureConfiguration {
private final FeatureFlagService featureFlagService;
private final ObjectMapper objectMapper;
@Value("${app.features.default-timeout:5000}")
private int defaultTimeout;
public <T> T getFeatureConfig(String featureFlag, String userId, 
Class<T> configClass, T defaultValue) {
try {
String treatment = featureFlagService.getFeatureTreatment(
featureFlag, userId);
if (featureFlagService.isFeatureEnabled(featureFlag, userId)) {
// For complex configurations, you can return different config objects
return configClass.newInstance();
}
// Parse configuration from treatment string
if (!treatment.equals(FeatureFlags.TREATMENT_CONTROL) && 
!treatment.equals(FeatureFlags.TREATMENT_OFF)) {
return objectMapper.readValue(treatment, configClass);
}
} catch (Exception e) {
log.warn("Failed to parse feature config for: {}", featureFlag, e);
}
return defaultValue;
}
public int getTimeout(String userId) {
Map<String, Object> attributes = Map.of("service", "api");
if (featureFlagService.isFeatureEnabled("dynamic_timeouts", 
userId, attributes)) {
String treatment = featureFlagService.getFeatureTreatment(
"dynamic_timeouts", userId, attributes);
try {
return Integer.parseInt(treatment);
} catch (NumberFormatException e) {
log.warn("Invalid timeout treatment: {}", treatment);
}
}
return defaultTimeout;
}
}

REST API Integration

1. Feature Flag Controller

@RestController
@RequestMapping("/api/features")
@Validated
public class FeatureFlagController {
private final FeatureFlagService featureFlagService;
@GetMapping("/{featureName}")
public ResponseEntity<FeatureFlagResponse> checkFeature(
@PathVariable String featureName,
@RequestParam String userId,
@RequestParam(required = false) String userTier,
@RequestParam(required = false) String country) {
Map<String, Object> attributes = new HashMap<>();
if (userTier != null) attributes.put("userTier", userTier);
if (country != null) attributes.put("country", country);
boolean enabled = featureFlagService.isFeatureEnabled(
featureName, userId, attributes);
String treatment = featureFlagService.getFeatureTreatment(
featureName, userId, attributes);
FeatureFlagResponse response = FeatureFlagResponse.builder()
.featureName(featureName)
.userId(userId)
.enabled(enabled)
.treatment(treatment)
.timestamp(Instant.now())
.build();
return ResponseEntity.ok(response);
}
@PostMapping("/batch")
public ResponseEntity<List<FeatureFlagResponse>> checkFeaturesBatch(
@RequestBody @Valid BatchFeatureRequest request) {
List<FeatureFlagResponse> responses = request.getFeatures().stream()
.map(featureName -> {
boolean enabled = featureFlagService.isFeatureEnabled(
featureName, request.getUserId(), request.getAttributes());
String treatment = featureFlagService.getFeatureTreatment(
featureName, request.getUserId(), request.getAttributes());
return FeatureFlagResponse.builder()
.featureName(featureName)
.userId(request.getUserId())
.enabled(enabled)
.treatment(treatment)
.timestamp(Instant.now())
.build();
})
.collect(Collectors.toList());
return ResponseEntity.ok(responses);
}
}

2. Request/Response DTOs

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FeatureFlagResponse {
private String featureName;
private String userId;
private boolean enabled;
private String treatment;
private Instant timestamp;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BatchFeatureRequest {
@NotBlank
private String userId;
@NotEmpty
private List<String> features;
private Map<String, Object> attributes;
}

Monitoring and Analytics

1. Feature Flag Metrics

@Component
public class FeatureFlagMetrics {
private final MeterRegistry meterRegistry;
private final FeatureFlagService featureFlagService;
private final Map<String, Counter> treatmentCounters = new ConcurrentHashMap<>();
@EventListener
public void onFeatureEvaluation(FeatureEvaluationEvent event) {
String counterName = String.format("feature_flag.treatment.%s.%s", 
event.getFeatureFlag(), 
event.getTreatment());
treatmentCounters
.computeIfAbsent(counterName, k -> 
meterRegistry.counter(k, "user_id", event.getUserId()))
.increment();
// Track evaluation timing
Timer.builder("feature_flag.evaluation.time")
.tag("feature_flag", event.getFeatureFlag())
.register(meterRegistry)
.record(event.getEvaluationTime());
}
@Scheduled(fixedRate = 60000) // Every minute
public void reportFeatureFlagSummary() {
SplitManager splitManager = // get SplitManager instance
List<SplitView> splits = splitManager.splits();
splits.forEach(split -> {
Gauge.builder("feature_flag.definition.exists")
.tag("feature_flag", split.getName())
.register(meterRegistry, 1);
});
}
}

2. Custom Event Tracking

@Service
@Slf4j
public class FeatureFlagEventService {
private final SplitClient splitClient;
public void trackFeatureConversion(String featureFlag, String userId, 
double value) {
Map<String, Object> properties = Map.of(
"conversion_value", value,
"tracking_timestamp", System.currentTimeMillis()
);
splitClient.track(userId, "user", "conversion", properties);
}
public void trackCustomEvent(String userId, String eventType,
Map<String, Object> properties) {
try {
splitClient.track(userId, "user", eventType, properties);
log.debug("Tracked event: {} for user: {}", eventType, userId);
} catch (Exception e) {
log.error("Failed to track event: {} for user: {}", 
eventType, userId, e);
}
}
}

Testing Strategies

1. Unit Tests with Mocking

@ExtendWith(MockitoExtension.class)
class FeatureFlagServiceTest {
@Mock
private SplitClient splitClient;
@Mock
private SplitManager splitManager;
@InjectMocks
private FeatureFlagService featureFlagService;
@Test
void shouldReturnTrueWhenFeatureIsEnabled() {
// Given
String featureFlag = "test_feature";
String userId = "user123";
when(splitClient.getTreatment(userId, featureFlag))
.thenReturn("on");
// When
boolean result = featureFlagService.isFeatureEnabled(featureFlag, userId);
// Then
assertTrue(result);
verify(splitClient).getTreatment(userId, featureFlag);
}
@Test
void shouldUseAttributesWhenProvided() {
// Given
String featureFlag = "test_feature";
String userId = "user123";
Map<String, Object> attributes = Map.of("userTier", "premium");
when(splitClient.getTreatment(userId, featureFlag, attributes))
.thenReturn("on");
// When
boolean result = featureFlagService.isFeatureEnabled(
featureFlag, userId, attributes);
// Then
assertTrue(result);
verify(splitClient).getTreatment(userId, featureFlag, attributes);
}
}

2. Integration Tests

@SpringBootTest
@TestPropertySource(properties = {
"splitio.api-key=localhost",
"splitio.enabled=true"
})
class SplitIOIntegrationTest {
@Autowired
private FeatureFlagService featureFlagService;
@Test
void shouldConnectToSplitIO() {
// This test verifies the integration with Split.io
// It requires a valid API key or localhost mode
assertDoesNotThrow(() -> {
boolean result = featureFlagService.isFeatureEnabled(
"test_feature", "test_user");
// We don't care about the result, just that no exception is thrown
});
}
}

Best Practices and Patterns

1. Feature Flag Management

@Component
public class FeatureFlagGuard {
private final FeatureFlagService featureFlagService;
private final CircuitBreaker circuitBreaker;
public boolean isAllowed(String featureFlag, String userId, 
Supplier<Boolean> fallback) {
try {
return circuitBreaker.executeSupplier(() -> 
featureFlagService.isFeatureEnabled(featureFlag, userId));
} catch (Exception e) {
log.warn("Feature flag check failed for: {}, using fallback", 
featureFlag, e);
return fallback.get();
}
}
public void executeWithFeature(String featureFlag, String userId,
Runnable enabledAction,
Runnable disabledAction) {
if (isAllowed(featureFlag, userId, () -> false)) {
enabledAction.run();
} else {
disabledAction.run();
}
}
}

2. Cleanup and Maintenance

@Service
@Slf4j
public class FeatureFlagCleanupService {
private final SplitManager splitManager;
@Scheduled(cron = "0 0 2 * * ?") // Daily at 2 AM
public void auditFeatureFlags() {
List<SplitView> splits = splitManager.splits();
splits.forEach(split -> {
if (isFlagStale(split)) {
log.warn("Stale feature flag detected: {}", split.getName());
// Notify team about stale flags
notifyStaleFlag(split.getName());
}
});
}
private boolean isFlagStale(SplitView split) {
// Implement logic to detect stale flags
// e.g., flags that have been 100% rolled out for too long
return false;
}
}

Conclusion

Implementing Split.io feature flags in Java provides powerful capabilities for controlled feature rollouts, A/B testing, and dynamic configuration management. By following these patterns and best practices, teams can safely deploy features, experiment with different implementations, and quickly respond to production issues without requiring code deployments.

The key to successful feature flag implementation is proper organization, comprehensive monitoring, and regular cleanup to avoid technical debt from accumulated feature flags.

Leave a Reply

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


Macro Nepal Helper