Feature flags (feature toggles) are a powerful technique that allows you to deploy code changes safely, perform A/B testing, and manage feature releases without redeploying your application. Split.io is a leading feature flag and experimentation platform that provides sophisticated targeting, real-time control, and detailed metrics.
What are Feature Flags?
Feature flags are conditional statements that control whether features are enabled for specific users, environments, or conditions. They enable:
- Trunk-based development without feature branches
- Gradual rollouts and canary releases
- Instant kill switches for problematic features
- A/B testing and experimentation
- User-specific targeting and personalization
Split.io Architecture
[Java Application] → [Split SDK] → [Split Cloud] ← [Split UI] | | | | Check feature Local cache Rule storage Management flags locally with polling and targeting console
Hands-On Tutorial: Implementing Split.io in Spring Boot
Let's build a complete e-commerce application with feature flags for controlled rollouts, experimentation, and operational control.
Step 1: Project Setup and Dependencies
Maven Dependencies (pom.xml):
<properties> <spring-boot.version>3.2.0</spring-boot.version> <split.version>4.4.2</split.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Split.io SDK --> <dependency> <groupId>io.split.client</groupId> <artifactId>split-client-java</artifactId> <version>${split.version}</version> </dependency> <!-- Configuration --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <!-- Caching --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Monitoring --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> </dependencies> Step 2: Configuration
application.yml:
split: io: api-key: ${SPLIT_IO_API_KEY:localhost} # Use 'localhost' for development without API key environment: ${SPLIT_IO_ENVIRONMENT:development} # Configuration options features-refresh-rate: 30 segments-refresh-rate: 60 impressions-refresh-rate: 30 metrics-refresh-rate: 30 connection-timeout: 15000 read-timeout: 15000 # Localhost mode (for development) localhost-feature-flags: new-checkout-ui: on premium-features: off recommendation-engine: on:user_premium search-v2: off promotional-banner: on:50% app: feature-flags: # Feature names new-checkout-ui: new_checkout_ui premium-features: premium_features recommendation-engine: recommendation_engine_v2 search-v2: search_algorithm_v2 promotional-banner: promotional_banner_holiday dark-mode: dark_mode_toggle payment-methods: new_payment_methods Split.io Configuration Class:
@Configuration @ConfigurationProperties(prefix = "split.io") @Validated public class SplitIOConfig { @NotBlank private String apiKey; private String environment = "development"; private int featuresRefreshRate = 30; private int segmentsRefreshRate = 60; private int impressionsRefreshRate = 30; private int metricsRefreshRate = 30; private int connectionTimeout = 15000; private int readTimeout = 15000; private Map<String, String> localhostFeatureFlags = new HashMap<>(); // Getters and setters public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getEnvironment() { return environment; } public void setEnvironment(String environment) { this.environment = environment; } public int getFeaturesRefreshRate() { return featuresRefreshRate; } public void setFeaturesRefreshRate(int featuresRefreshRate) { this.featuresRefreshRate = featuresRefreshRate; } public int getSegmentsRefreshRate() { return segmentsRefreshRate; } public void setSegmentsRefreshRate(int segmentsRefreshRate) { this.segmentsRefreshRate = segmentsRefreshRate; } public int getImpressionsRefreshRate() { return impressionsRefreshRate; } public void setImpressionsRefreshRate(int impressionsRefreshRate) { this.impressionsRefreshRate = impressionsRefreshRate; } public int getMetricsRefreshRate() { return metricsRefreshRate; } public void setMetricsRefreshRate(int metricsRefreshRate) { this.metricsRefreshRate = metricsRefreshRate; } public int getConnectionTimeout() { return connectionTimeout; } public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } public int getReadTimeout() { return readTimeout; } public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; } public Map<String, String> getLocalhostFeatureFlags() { return localhostFeatureFlags; } public void setLocalhostFeatureFlags(Map<String, String> localhostFeatureFlags) { this.localhostFeatureFlags = localhostFeatureFlags; } } Step 3: Split.io Client Factory
@Configuration @EnableCaching public class SplitIOConfiguration { private static final Logger logger = LoggerFactory.getLogger(SplitIOConfiguration.class); private final SplitIOConfig config; public SplitIOConfiguration(SplitIOConfig config) { this.config = config; } @Bean @Primary public SplitClient splitClient() { try { SplitClientConfig clientConfig = SplitClientConfig.builder() .setBlockUntilReadyTimeout(10000) .featuresRefreshRate(config.getFeaturesRefreshRate()) .segmentsRefreshRate(config.getSegmentsRefreshRate()) .impressionsRefreshRate(config.getImpressionsRefreshRate()) .metricsRefreshRate(config.getMetricsRefreshRate()) .connectionTimeout(config.getConnectionTimeout()) .readTimeout(config.getReadTimeout()) .build(); SplitFactory splitFactory; if ("localhost".equals(config.getApiKey())) { logger.info("Initializing Split.io in LOCALHOST mode"); splitFactory = localhostSplitFactory(); } else { logger.info("Initializing Split.io in PRODUCTION mode"); splitFactory = SplitFactoryBuilder.build(config.getApiKey(), clientConfig); } splitFactory.client().blockUntilReady(); logger.info("Split.io client initialized successfully"); return splitFactory.client(); } catch (Exception e) { logger.error("Failed to initialize Split.io client", e); throw new RuntimeException("Split.io initialization failed", e); } } @Bean public SplitManager splitManager(SplitClient splitClient) { return splitClient.getSplitManager(); } /** * Localhost factory for development without API key */ private SplitFactory localhostSplitFactory() { Map<String, String> featureFlags = config.getLocalhostFeatureFlags(); LocalhostSplitFactory localhostFactory = new LocalhostSplitFactory(); // Set up local feature flags featureFlags.forEach((featureName, treatment) -> { localhostFactory.getFeatureFlagRegistry().register( featureName, parseLocalhostTreatment(treatment) ); }); return localhostFactory; } /** * Parse localhost treatment strings like "on", "off", "on:50%", "on:user_premium" */ private Consumer<LocalhostSplit> parseLocalhostTreatment(String treatment) { if ("on".equalsIgnoreCase(treatment)) { return split -> split.setTreatment("on"); } else if ("off".equalsIgnoreCase(treatment)) { return split -> split.setTreatment("off"); } else if (treatment.startsWith("on:")) { String target = treatment.substring(3); if (target.endsWith("%")) { // Percentage rollout int percentage = Integer.parseInt(target.substring(0, target.length() - 1)); return split -> split.setTreatment("on", percentage); } else { // Targeted rollout return split -> split.setTreatment("on", Collections.singleton(target)); } } return split -> split.setTreatment("off"); } @PreDestroy public void destroy() { try { if (splitClient() != null) { splitClient().destroy(); logger.info("Split.io client destroyed successfully"); } } catch (Exception e) { logger.error("Error destroying Split.io client", e); } } } Step 4: Feature Flag Service
@Service @Slf4j public class FeatureFlagService { private final SplitClient splitClient; private final SplitManager splitManager; private final ObjectMapper objectMapper; // Feature flag names (should match Split.io dashboard) public static final String NEW_CHECKOUT_UI = "new_checkout_ui"; public static final String PREMIUM_FEATURES = "premium_features"; public static final String RECOMMENDATION_ENGINE_V2 = "recommendation_engine_v2"; public static final String SEARCH_ALGORITHM_V2 = "search_algorithm_v2"; public static final String PROMOTIONAL_BANNER = "promotional_banner_holiday"; public static final String DARK_MODE = "dark_mode_toggle"; public static final String NEW_PAYMENT_METHODS = "new_payment_methods"; public FeatureFlagService(SplitClient splitClient, SplitManager splitManager, ObjectMapper objectMapper) { this.splitClient = splitClient; this.splitManager = splitManager; this.objectMapper = objectMapper; } /** * Basic feature flag check with user context */ public boolean isFeatureEnabled(String featureFlag, UserContext userContext) { return "on".equals(getTreatment(featureFlag, userContext)); } /** * Get treatment with user context */ public String getTreatment(String featureFlag, UserContext userContext) { try { String treatment = splitClient.getTreatment( userContext.getUserId(), featureFlag, userContext.getAttributes() ); log.debug("Feature flag '{}' treatment for user {}: {}", featureFlag, userContext.getUserId(), treatment); return treatment; } catch (Exception e) { log.error("Error getting treatment for feature flag: {}", featureFlag, e); return "off"; // Fail safe } } /** * Get treatment with JSON configuration */ public <T> T getTreatmentWithConfig(String featureFlag, UserContext userContext, Class<T> configType) { try { SplitResult result = splitClient.getTreatmentWithConfig( userContext.getUserId(), featureFlag, userContext.getAttributes() ); log.debug("Feature flag '{}' treatment with config for user {}: {}", featureFlag, userContext.getUserId(), result.treatment()); // Parse configuration if provided if ("on".equals(result.treatment()) && result.config() != null && !result.config().trim().isEmpty()) { try { return objectMapper.readValue(result.config(), configType); } catch (Exception e) { log.error("Failed to parse feature flag config for {}: {}", featureFlag, result.config(), e); } } return null; } catch (Exception e) { log.error("Error getting treatment with config for feature flag: {}", featureFlag, e); return null; } } /** * Track feature flag exposure for analytics */ public void trackExposure(String featureFlag, UserContext userContext, String treatment) { try { splitClient.track( userContext.getUserId(), "user", "feature_flag_exposure", null, // value createEventProperties(featureFlag, treatment, userContext) ); log.debug("Tracked feature flag exposure: {} - {} for user {}", featureFlag, treatment, userContext.getUserId()); } catch (Exception e) { log.error("Error tracking feature flag exposure: {}", featureFlag, e); } } /** * Get all available feature flags */ public List<SplitView> getAllFeatureFlags() { try { return splitManager.splits(); } catch (Exception e) { log.error("Error getting feature flags list", e); return Collections.emptyList(); } } /** * Check if feature flag exists */ public boolean featureFlagExists(String featureFlag) { try { return splitManager.split(featureFlag) != null; } catch (Exception e) { log.error("Error checking if feature flag exists: {}", featureFlag, e); return false; } } /** * Bulk check multiple feature flags */ public Map<String, Boolean> bulkCheckFeatures(UserContext userContext, String... featureFlags) { Map<String, Boolean> results = new HashMap<>(); for (String featureFlag : featureFlags) { results.put(featureFlag, isFeatureEnabled(featureFlag, userContext)); } return results; } /** * Feature flag with percentage rollout */ public boolean isFeatureEnabledForPercentage(String featureFlag, UserContext userContext, int fallbackPercentage) { String treatment = getTreatment(featureFlag, userContext); if ("on".equals(treatment)) { return true; } else if ("off".equals(treatment)) { return false; } else { // Handle percentage-based treatments return isUserInPercentage(userContext.getUserId(), fallbackPercentage); } } private boolean isUserInPercentage(String userId, int percentage) { // Simple hash-based percentage calculation int hash = Math.abs(userId.hashCode()) % 100; return hash < percentage; } private Map<String, Object> createEventProperties(String featureFlag, String treatment, UserContext userContext) { Map<String, Object> properties = new HashMap<>(); properties.put("feature_flag", featureFlag); properties.put("treatment", treatment); properties.put("user_id", userContext.getUserId()); properties.put("timestamp", Instant.now().toString()); properties.put("environment", getEnvironment()); // Add user attributes for segmentation if (userContext.getAttributes() != null) { properties.putAll(userContext.getAttributes()); } return properties; } private String getEnvironment() { return System.getenv().getOrDefault("SPLIT_ENV", "development"); } } Step 5: User Context Model
public class UserContext { private final String userId; private final Map<String, Object> attributes; public UserContext(String userId) { this(userId, new HashMap<>()); } public UserContext(String userId, Map<String, Object> attributes) { this.userId = userId; this.attributes = attributes != null ? new HashMap<>(attributes) : new HashMap<>(); } // Builder pattern for fluent creation public static UserContext of(String userId) { return new UserContext(userId); } public UserContext withAttribute(String key, Object value) { this.attributes.put(key, value); return this; } public UserContext withAttributes(Map<String, Object> additionalAttributes) { this.attributes.putAll(additionalAttributes); return this; } public UserContext withPlan(String plan) { return withAttribute("plan", plan); } public UserContext withCountry(String country) { return withAttribute("country", country); } public UserContext withDevice(String device) { return withAttribute("device", device); } public UserContext withRegistrationDate(LocalDate registrationDate) { return withAttribute("registration_date", registrationDate.toString()); } // Getters public String getUserId() { return userId; } public Map<String, Object> getAttributes() { return Collections.unmodifiableMap(attributes); } // Common attribute keys public static class Attributes { public static final String PLAN = "plan"; public static final String COUNTRY = "country"; public static final String DEVICE = "device"; public static final String REGISTRATION_DATE = "registration_date"; public static final String USER_AGE = "user_age"; public static final String SEGMENT = "segment"; } // Common user segments public static class Segments { public static final String BETA_TESTERS = "beta_testers"; public static final String PREMIUM_USERS = "premium_users"; public static final String NEW_USERS = "new_users"; public static final String POWER_USERS = "power_users"; } } Step 6: Feature-Specific Services
Checkout Service with Feature Flags:
@Service @Slf4j public class CheckoutService { private final FeatureFlagService featureFlagService; private final PaymentService paymentService; public CheckoutService(FeatureFlagService featureFlagService, PaymentService paymentService) { this.featureFlagService = featureFlagService; this.paymentService = paymentService; } /** * Process checkout with feature-flagged improvements */ public CheckoutResult processCheckout(CheckoutRequest request, UserContext userContext) { log.info("Processing checkout for user: {}", userContext.getUserId()); // Check if new checkout UI is enabled for this user boolean newCheckoutEnabled = featureFlagService.isFeatureEnabled( FeatureFlagService.NEW_CHECKOUT_UI, userContext ); // Track exposure for analytics featureFlagService.trackExposure( FeatureFlagService.NEW_CHECKOUT_UI, userContext, newCheckoutEnabled ? "on" : "off" ); CheckoutResult result; if (newCheckoutEnabled) { result = processCheckoutV2(request, userContext); } else { result = processCheckoutV1(request, userContext); } return result; } /** * New checkout flow with additional features */ private CheckoutResult processCheckoutV2(CheckoutRequest request, UserContext userContext) { log.info("Using new checkout flow for user: {}", userContext.getUserId()); // Check for additional feature flags boolean newPaymentMethods = featureFlagService.isFeatureEnabled( FeatureFlagService.NEW_PAYMENT_METHODS, userContext ); CheckoutConfig config = featureFlagService.getTreatmentWithConfig( FeatureFlagService.NEW_CHECKOUT_UI, userContext, CheckoutConfig.class ); // Apply configuration from feature flag if (config != null) { applyCheckoutConfig(request, config); } // Process with new payment methods if enabled if (newPaymentMethods) { return paymentService.processWithNewMethods(request, userContext); } else { return paymentService.processWithLegacyMethods(request, userContext); } } /** * Legacy checkout flow */ private CheckoutResult processCheckoutV1(CheckoutRequest request, UserContext userContext) { log.info("Using legacy checkout flow for user: {}", userContext.getUserId()); return paymentService.processWithLegacyMethods(request, userContext); } private void applyCheckoutConfig(CheckoutRequest request, CheckoutConfig config) { if (config.getMaxCartSize() > 0 && request.getItems().size() > config.getMaxCartSize()) { throw new CartSizeExceededException("Cart size exceeds maximum allowed"); } if (config.isExpressCheckoutEnabled()) { request.setExpressCheckout(true); } } // Configuration DTO for feature flag public static class CheckoutConfig { private boolean expressCheckoutEnabled; private int maxCartSize = 50; private double discountThreshold = 100.0; private List<String> allowedPaymentMethods; // Getters and setters public boolean isExpressCheckoutEnabled() { return expressCheckoutEnabled; } public void setExpressCheckoutEnabled(boolean expressCheckoutEnabled) { this.expressCheckoutEnabled = expressCheckoutEnabled; } public int getMaxCartSize() { return maxCartSize; } public void setMaxCartSize(int maxCartSize) { this.maxCartSize = maxCartSize; } public double getDiscountThreshold() { return discountThreshold; } public void setDiscountThreshold(double discountThreshold) { this.discountThreshold = discountThreshold; } public List<String> getAllowedPaymentMethods() { return allowedPaymentMethods; } public void setAllowedPaymentMethods(List<String> allowedPaymentMethods) { this.allowedPaymentMethods = allowedPaymentMethods; } } } Recommendation Service with Experimentation:
@Service @Slf4j public class RecommendationService { private final FeatureFlagService featureFlagService; private final LegacyRecommendationEngine legacyEngine; private final AIV2RecommendationEngine aiV2Engine; public RecommendationService(FeatureFlagService featureFlagService, LegacyRecommendationEngine legacyEngine, AIV2RecommendationEngine aiV2Engine) { this.featureFlagService = featureFlagService; this.legacyEngine = legacyEngine; this.aiV2Engine = aiV2Engine; } /** * Get recommendations with A/B testing between algorithms */ public List<Product> getRecommendations(UserContext userContext, String context) { String treatment = featureFlagService.getTreatment( FeatureFlagService.RECOMMENDATION_ENGINE_V2, userContext ); // Track which algorithm was used featureFlagService.trackExposure( FeatureFlagService.RECOMMENDATION_ENGINE_V2, userContext, treatment ); List<Product> recommendations; switch (treatment) { case "on": recommendations = aiV2Engine.getRecommendations(userContext.getUserId(), context); break; case "off": recommendations = legacyEngine.getRecommendations(userContext.getUserId(), context); break; case "v3": // Experimental third algorithm recommendations = getExperimentalRecommendations(userContext, context); break; default: recommendations = legacyEngine.getRecommendations(userContext.getUserId(), context); break; } // Track recommendation performance trackRecommendationMetrics(userContext, treatment, recommendations.size()); return recommendations; } /** * Get personalized recommendations with configuration */ public List<Product> getPersonalizedRecommendations(UserContext userContext, RecommendationRequest request) { RecommendationConfig config = featureFlagService.getTreatmentWithConfig( FeatureFlagService.RECOMMENDATION_ENGINE_V2, userContext, RecommendationConfig.class ); if (config != null) { return getRecommendationsWithConfig(userContext, request, config); } else { return getRecommendations(userContext, request.getContext()); } } private List<Product> getRecommendationsWithConfig(UserContext userContext, RecommendationRequest request, RecommendationConfig config) { // Apply configuration from feature flag request.setMaxResults(config.getMaxRecommendations()); request.setDiversityFactor(config.getDiversityFactor()); if (config.isPersonalizationEnabled()) { return aiV2Engine.getPersonalizedRecommendations(userContext.getUserId(), request); } else { return legacyEngine.getRecommendations(userContext.getUserId(), request.getContext()); } } private List<Product> getExperimentalRecommendations(UserContext userContext, String context) { // Experimental algorithm - could be another service log.info("Using experimental recommendation algorithm for user: {}", userContext.getUserId()); return legacyEngine.getRecommendations(userContext.getUserId(), context); } private void trackRecommendationMetrics(UserContext userContext, String treatment, int count) { try { Map<String, Object> properties = new HashMap<>(); properties.put("treatment", treatment); properties.put("recommendation_count", count); properties.put("user_segment", userContext.getAttributes().get("segment")); featureFlagService.track( "recommendations_generated", userContext, properties ); } catch (Exception e) { log.error("Failed to track recommendation metrics", e); } } // Configuration DTO public static class RecommendationConfig { private int maxRecommendations = 10; private double diversityFactor = 0.3; private boolean personalizationEnabled = true; private double confidenceThreshold = 0.7; // Getters and setters public int getMaxRecommendations() { return maxRecommendations; } public void setMaxRecommendations(int maxRecommendations) { this.maxRecommendations = maxRecommendations; } public double getDiversityFactor() { return diversityFactor; } public void setDiversityFactor(double diversityFactor) { this.diversityFactor = diversityFactor; } public boolean isPersonalizationEnabled() { return personalizationEnabled; } public void setPersonalizationEnabled(boolean personalizationEnabled) { this.personalizationEnabled = personalizationEnabled; } public double getConfidenceThreshold() { return confidenceThreshold; } public void setConfidenceThreshold(double confidenceThreshold) { this.confidenceThreshold = confidenceThreshold; } } } Step 7: REST Controllers
@RestController @RequestMapping("/api") @Slf4j public class EcommerceController { private final CheckoutService checkoutService; private final RecommendationService recommendationService; private final FeatureFlagService featureFlagService; public EcommerceController(CheckoutService checkoutService, RecommendationService recommendationService, FeatureFlagService featureFlagService) { this.checkoutService = checkoutService; this.recommendationService = recommendationService; this.featureFlagService = featureFlagService; } @PostMapping("/checkout") public ResponseEntity<CheckoutResult> checkout(@RequestBody CheckoutRequest request, @RequestHeader("X-User-Id") String userId) { UserContext userContext = UserContext.of(userId) .withPlan("premium") .withCountry("US") .withDevice("web"); CheckoutResult result = checkoutService.processCheckout(request, userContext); return ResponseEntity.ok(result); } @GetMapping("/recommendations") public ResponseEntity<List<Product>> getRecommendations( @RequestParam(defaultValue = "homepage") String context, @RequestHeader("X-User-Id") String userId) { UserContext userContext = UserContext.of(userId) .withAttribute("context", context); List<Product> recommendations = recommendationService.getRecommendations(userContext, context); return ResponseEntity.ok(recommendations); } @GetMapping("/features") public ResponseEntity<Map<String, Object>> getFeatureFlags(@RequestHeader("X-User-Id") String userId) { UserContext userContext = UserContext.of(userId); Map<String, Boolean> features = featureFlagService.bulkCheckFeatures(userContext, FeatureFlagService.NEW_CHECKOUT_UI, FeatureFlagService.PREMIUM_FEATURES, FeatureFlagService.RECOMMENDATION_ENGINE_V2, FeatureFlagService.SEARCH_ALGORITHM_V2, FeatureFlagService.PROMOTIONAL_BANNER, FeatureFlagService.DARK_MODE ); Map<String, Object> response = new HashMap<>(); response.put("userId", userId); response.put("features", features); response.put("timestamp", Instant.now().toString()); return ResponseEntity.ok(response); } @GetMapping("/features/{featureName}") public ResponseEntity<FeatureFlagResponse> getFeatureFlag( @PathVariable String featureName, @RequestHeader("X-User-Id") String userId) { UserContext userContext = UserContext.of(userId); String treatment = featureFlagService.getTreatment(featureName, userContext); FeatureFlagResponse response = new FeatureFlagResponse(); response.setFeatureName(featureName); response.setUserId(userId); response.setTreatment(treatment); response.setEnabled("on".equals(treatment)); response.setTimestamp(Instant.now().toString()); return ResponseEntity.ok(response); } } // Response DTOs class FeatureFlagResponse { private String featureName; private String userId; private String treatment; private boolean enabled; private String timestamp; // Getters and setters public String getFeatureName() { return featureName; } public void setFeatureName(String featureName) { this.featureName = featureName; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getTreatment() { return treatment; } public void setTreatment(String treatment) { this.treatment = treatment; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getTimestamp() { return timestamp; } public void setTimestamp(String timestamp) { this.timestamp = timestamp; } } Step 8: Monitoring and Health Checks
@Component public class SplitIOHealthIndicator implements HealthIndicator { private final SplitClient splitClient; private final SplitManager splitManager; public SplitIOHealthIndicator(SplitClient splitClient, SplitManager splitManager) { this.splitClient = splitClient; this.splitManager = splitManager; } @Override public Health health() { try { // Check if client is ready if (!splitClient.isReady()) { return Health.down() .withDetail("service", "split.io") .withDetail("status", "not_ready") .build(); } // Get some basic metrics List<SplitView> splits = splitManager.splits(); long activeSplits = splits.stream() .filter(split -> split.status.equals("ACTIVE")) .count(); Map<String, Object> details = new HashMap<>(); details.put("total_splits", splits.size()); details.put("active_splits", activeSplits); details.put("ready", true); return Health.up() .withDetail("service", "split.io") .withDetails(details) .build(); } catch (Exception e) { return Health.down() .withDetail("service", "split.io") .withDetail("error", e.getMessage()) .build(); } } } @RestController @RequestMapping("/api/admin") @Slf4j public class FeatureFlagAdminController { private final FeatureFlagService featureFlagService; private final SplitManager splitManager; public FeatureFlagAdminController(FeatureFlagService featureFlagService, SplitManager splitManager) { this.featureFlagService = featureFlagService; this.splitManager = splitManager; } @GetMapping("/features") public ResponseEntity<List<SplitView>> getAllFeatures() { try { List<SplitView> features = featureFlagService.getAllFeatureFlags(); return ResponseEntity.ok(features); } catch (Exception e) { log.error("Failed to get feature flags", e); return ResponseEntity.status(500).build(); } } @PostMapping("/features/{featureName}/test") public ResponseEntity<Map<String, Object>> testFeatureFlag( @PathVariable String featureName, @RequestBody TestFeatureRequest request) { try { UserContext userContext = UserContext.of(request.getUserId()) .withAttributes(request.getAttributes()); String treatment = featureFlagService.getTreatment(featureName, userContext); Map<String, Object> response = new HashMap<>(); response.put("featureName", featureName); response.put("userId", request.getUserId()); response.put("treatment", treatment); response.put("enabled", "on".equals(treatment)); response.put("attributes", request.getAttributes()); return ResponseEntity.ok(response); } catch (Exception e) { log.error("Failed to test feature flag: {}", featureName, e); return ResponseEntity.status(500).build(); } } } class TestFeatureRequest { private String userId; private Map<String, Object> attributes; // Getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public Map<String, Object> getAttributes() { return attributes; } public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; } } Step 9: Testing Utilities
@Configuration @Profile("test") public class TestSplitIOConfiguration { @Bean public SplitClient testSplitClient() { return new TestSplitClient(); } @Bean public SplitManager testSplitManager() { return new TestSplitManager(); } /** * Test implementation for unit tests */ public static class TestSplitClient implements SplitClient { private final Map<String, String> featureFlags = new HashMap<>(); public TestSplitClient() { // Default test flags featureFlags.put("new_checkout_ui", "off"); featureFlags.put("premium_features", "on"); featureFlags.put("recommendation_engine_v2", "off"); } @Override public String getTreatment(String key, String splitName) { return featureFlags.getOrDefault(splitName, "off"); } @Override public String getTreatment(String key, String splitName, Map<String, Object> attributes) { return getTreatment(key, splitName); } @Override public SplitResult getTreatmentWithConfig(String key, String splitName, Map<String, Object> attributes) { String treatment = getTreatment(key, splitName); return new SplitResult(treatment, null); } // Implement other methods with default behavior... @Override public void destroy() {} @Override public boolean track(String key, String trafficType, String eventType) { return true; } @Override public boolean track(String key, String trafficType, String eventType, double value) { return true; } @Override public boolean track(String key, String trafficType, String eventType, Map<String, Object> properties) { return true; } @Override public boolean track(String key, String trafficType, String eventType, double value, Map<String, Object> properties) { return true; } @Override public void blockUntilReady() {} @Override public boolean isReady() { return true; } // Helper method for tests public void setTreatment(String featureFlag, String treatment) { featureFlags.put(featureFlag, treatment); } } public static class TestSplitManager implements SplitManager { @Override public List<SplitView> splits() { return Collections.emptyList(); } @Override public SplitView split(String splitName) { return null; } @Override public List<String> splitNames() { return Collections.emptyList(); } @Override public void blockUntilReady() {} } } Running the Application
- Get Split.io API Key:
- Sign up at Split.io
- Create your feature flags in the dashboard
- Get your API key from workspace settings
- Set environment variable:
export SPLIT_IO_API_KEY=your_api_key_here
- Run the application:
mvn spring-boot:run
- Test feature flags:
curl -H "X-User-Id: user123" http://localhost:8080/api/features/new_checkout_ui
Best Practices
1. Naming Conventions
// Good naming "checkout_ui_v2" "search_algorithm_2024" "promo_banner_holiday" // Avoid "new_feature" // Too vague "test_flag" // Not descriptive
2. Flag Lifecycle Management
- Development: Localhost mode with hardcoded treatments
- Staging: Targeted to internal users
- Production: Gradual rollout with metrics
- Cleanup: Remove flags after full rollout
3. Monitoring and Alerting
- Track feature flag exposure rates
- Monitor performance impact of new features
- Set up alerts for unexpected treatment distributions
- Log feature flag decisions for debugging
Conclusion
Implementing Split.io feature flags in Java provides:
- Safe deployments with instant rollback capabilities
- Gradual feature rollouts to mitigate risk
- A/B testing infrastructure for data-driven decisions
- User segmentation for personalized experiences
- Operational control without code deployments
By following this comprehensive implementation, you can build a robust feature flag system that enables continuous delivery while maintaining stability and control over your production environment.