google-site-verification: google61fe8ba583a51912.html
Dark Launch Strategy in Java: Complete Implementation Guide

Introduction

Dark Launching (also known as Feature Flagging or Gradual Rollouts) is a deployment strategy where new features are released to a subset of users before making them available to everyone. This allows for testing in production, gathering real-user metrics, and mitigating risks associated with new feature releases.

This guide covers implementing a comprehensive dark launch strategy in Java with feature flags, gradual rollouts, A/B testing, and operational controls.


Architecture Overview

[Application] → [Feature Flag Client] → [Flag Management Service] → [Configuration Store] ↓ ↓ ↓ ↓ Feature Checks Local Cache Rules Engine Redis/Database User Context Fallback Logic Targeting File System A/B Testing Metrics Collection Gradual Rollouts Environment Vars

Step 1: Project Dependencies

<properties> <spring-boot.version>3.2.0</spring-boot.version> <redis.version>3.2.0</redis.version> <micrometer.version>1.12.0</micrometer.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-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Redis for feature flag storage --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Cache --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>${micrometer.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>${micrometer.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies>

Step 2: Configuration

application.yml

feature: flags: enabled: true cache: ttl-minutes: 5 max-size: 1000 fallback: enabled: true default-enabled: false refresh: interval-seconds: 30 metrics: enabled: true prefix: "feature_flag" # Storage configuration storage: type: redis # Options: redis, in_memory, database redis: key-prefix: "feature_flags:" database: table-name: "feature_flags" # Targeting rules targeting: max-rules-per-flag: 50 enable-sticky-sessions: true # Redis configuration spring: data: redis: host: localhost port: 6379 password: "" database: 0 cache: type: redis redis: time-to-live: 300000 # 5 minutes # Management endpoints management: endpoints: web: exposure: include: health,metrics,featureflags endpoint: featureflags: enabled: true # Logging logging: level: com.example.featureflags: DEBUG

Step 3: Core Data Models

Feature Flag Models

FeatureFlag.java

package com.example.featureflags.model; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.*; public class FeatureFlag { @NotBlank private String key; private String name; private String description; @NotNull private Boolean enabled = false; private RolloutStrategy rolloutStrategy = RolloutStrategy.GRADUAL; private Double rolloutPercentage = 0.0; private List<TargetingRule> targetingRules = new ArrayList<>(); private Map<String, Object> variants = new HashMap<>(); private String defaultVariant = "control"; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createdAt; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updatedAt; private String createdBy; private Map<String, Object> metadata = new HashMap<>(); // Constructors public FeatureFlag() {} public FeatureFlag(String key, String name, boolean enabled) { this.key = key; this.name = name; this.enabled = enabled; this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } // Getters and Setters public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public RolloutStrategy getRolloutStrategy() { return rolloutStrategy; } public void setRolloutStrategy(RolloutStrategy rolloutStrategy) { this.rolloutStrategy = rolloutStrategy; } public Double getRolloutPercentage() { return rolloutPercentage; } public void setRolloutPercentage(Double rolloutPercentage) { this.rolloutPercentage = rolloutPercentage; } public List<TargetingRule> getTargetingRules() { return targetingRules; } public void setTargetingRules(List<TargetingRule> targetingRules) { this.targetingRules = targetingRules; } public Map<String, Object> getVariants() { return variants; } public void setVariants(Map<String, Object> variants) { this.variants = variants; } public String getDefaultVariant() { return defaultVariant; } public void setDefaultVariant(String defaultVariant) { this.defaultVariant = defaultVariant; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public String getCreatedBy() { return createdBy; } public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } public Map<String, Object> getMetadata() { return metadata; } public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; } // Helper methods public void addTargetingRule(TargetingRule rule) { if (targetingRules == null) { targetingRules = new ArrayList<>(); } targetingRules.add(rule); } public void addVariant(String name, Object value) { if (variants == null) { variants = new HashMap<>(); } variants.put(name, value); } public boolean hasVariants() { return variants != null && !variants.isEmpty(); } }

TargetingRule.java

package com.example.featureflags.model; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import java.util.List; import java.util.Map; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = UserIdRule.class, name = "USER_ID"), @JsonSubTypes.Type(value = PercentageRule.class, name = "PERCENTAGE"), @JsonSubTypes.Type(value = UserPropertyRule.class, name = "USER_PROPERTY"), @JsonSubTypes.Type(value = CohortRule.class, name = "COHORT"), @JsonSubTypes.Type(value = DateTimeRule.class, name = "DATETIME") }) public abstract class TargetingRule { private String name; private Boolean enabled = true; private String variant; public abstract boolean evaluate(EvaluationContext context); // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public String getVariant() { return variant; } public void setVariant(String variant) { this.variant = variant; } } // Specific rule implementations class UserIdRule extends TargetingRule { private List<String> userIds; private List<String> excludedUserIds; @Override public boolean evaluate(EvaluationContext context) { if (!getEnabled()) return false; String userId = context.getUserId(); if (userId == null) return false; if (excludedUserIds != null && excludedUserIds.contains(userId)) { return false; } return userIds != null && userIds.contains(userId); } // Getters and Setters public List<String> getUserIds() { return userIds; } public void setUserIds(List<String> userIds) { this.userIds = userIds; } public List<String> getExcludedUserIds() { return excludedUserIds; } public void setExcludedUserIds(List<String> excludedUserIds) { this.excludedUserIds = excludedUserIds; } } class PercentageRule extends TargetingRule { private Double percentage; private String salt; // For consistent hashing @Override public boolean evaluate(EvaluationContext context) { if (!getEnabled() || percentage == null) return false; String userId = context.getUserId(); if (userId == null) { // Fallback to random if no user ID return Math.random() * 100 < percentage; } // Consistent hashing based on user ID and salt String hashInput = (salt != null ? salt : "") + userId; int hash = Math.abs(hashInput.hashCode()) % 100; return hash < percentage; } // Getters and Setters public Double getPercentage() { return percentage; } public void setPercentage(Double percentage) { this.percentage = percentage; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } } class UserPropertyRule extends TargetingRule { private String property; private Object value; private Operator operator = Operator.EQUALS; @Override public boolean evaluate(EvaluationContext context) { if (!getEnabled() || property == null) return false; Object userValue = context.getUserProperties().get(property); if (userValue == null) return false; return operator.compare(userValue, value); } // Getters and Setters public String getProperty() { return property; } public void setProperty(String property) { this.property = property; } public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } public Operator getOperator() { return operator; } public void setOperator(Operator operator) { this.operator = operator; } } class CohortRule extends TargetingRule { private List<String> cohorts; @Override public boolean evaluate(EvaluationContext context) { if (!getEnabled() || cohorts == null) return false; List<String> userCohorts = context.getCohorts(); if (userCohorts == null) return false; return userCohorts.stream().anyMatch(cohorts::contains); } // Getters and Setters public List<String> getCohorts() { return cohorts; } public void setCohorts(List<String> cohorts) { this.cohorts = cohorts; } } class DateTimeRule extends TargetingRule { private LocalDateTime startTime; private LocalDateTime endTime; @Override public boolean evaluate(EvaluationContext context) { if (!getEnabled()) return false; LocalDateTime now = LocalDateTime.now(); boolean afterStart = startTime == null || now.isAfter(startTime); boolean beforeEnd = endTime == null || now.isBefore(endTime); return afterStart && beforeEnd; } // Getters and Setters public LocalDateTime getStartTime() { return startTime; } public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; } public LocalDateTime getEndTime() { return endTime; } public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; } }

Enums and Supporting Classes

Enums.java

package com.example.featureflags.model; public enum RolloutStrategy { GRADUAL, // Gradually increase percentage ALL_OR_NOTHING, // Either 0% or 100% TARGETED, // Specific users/groups only A_B_TESTING // A/B testing with variants } public enum Operator { EQUALS { @Override public boolean compare(Object a, Object b) { return a.equals(b); } }, NOT_EQUALS { @Override public boolean compare(Object a, Object b) { return !a.equals(b); } }, CONTAINS { @Override public boolean compare(Object a, Object b) { return a.toString().contains(b.toString()); } }, GREATER_THAN { @Override @SuppressWarnings("unchecked") public boolean compare(Object a, Object b) { if (a instanceof Comparable && b instanceof Comparable) { return ((Comparable<Object>) a).compareTo(b) > 0; } return false; } }, LESS_THAN { @Override @SuppressWarnings("unchecked") public boolean compare(Object a, Object b) { if (a instanceof Comparable && b instanceof Comparable) { return ((Comparable<Object>) a).compareTo(b) < 0; } return false; } }; public abstract boolean compare(Object a, Object b); }

EvaluationContext.java

package com.example.featureflags.model; import java.util.HashMap; import java.util.List; import java.util.Map; public class EvaluationContext { private String userId; private String sessionId; private String environment; private Map<String, Object> userProperties = new HashMap<>(); private List<String> cohorts; private Map<String, Object> attributes = new HashMap<>(); // Constructors public EvaluationContext() {} public EvaluationContext(String userId) { this.userId = userId; } public EvaluationContext(String userId, Map<String, Object> userProperties) { this.userId = userId; this.userProperties = userProperties != null ? userProperties : new HashMap<>(); } // Builder pattern public static Builder builder() { return new Builder(); } public static class Builder { private EvaluationContext context = new EvaluationContext(); public Builder userId(String userId) { context.userId = userId; return this; } public Builder sessionId(String sessionId) { context.sessionId = sessionId; return this; } public Builder environment(String environment) { context.environment = environment; return this; } public Builder userProperty(String key, Object value) { context.userProperties.put(key, value); return this; } public Builder userProperties(Map<String, Object> properties) { context.userProperties.putAll(properties); return this; } public Builder cohorts(List<String> cohorts) { context.cohorts = cohorts; return this; } public Builder attribute(String key, Object value) { context.attributes.put(key, value); return this; } public EvaluationContext build() { return context; } } // Getters and Setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getSessionId() { return sessionId; } public void setSessionId(String sessionId) { this.sessionId = sessionId; } public String getEnvironment() { return environment; } public void setEnvironment(String environment) { this.environment = environment; } public Map<String, Object> getUserProperties() { return userProperties; } public void setUserProperties(Map<String, Object> userProperties) { this.userProperties = userProperties; } public List<String> getCohorts() { return cohorts; } public void setCohorts(List<String> cohorts) { this.cohorts = cohorts; } public Map<String, Object> getAttributes() { return attributes; } public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; } // Helper methods public void addUserProperty(String key, Object value) { userProperties.put(key, value); } public Object getUserProperty(String key) { return userProperties.get(key); } }

FeatureEvaluationResult.java

package com.example.featureflags.model; public class FeatureEvaluationResult { private final boolean enabled; private final String variant; private final Object variantValue; private final String reason; private final String flagKey; public FeatureEvaluationResult(boolean enabled, String variant, Object variantValue, String reason, String flagKey) { this.enabled = enabled; this.variant = variant; this.variantValue = variantValue; this.reason = reason; this.flagKey = flagKey; } // Getters public boolean isEnabled() { return enabled; } public String getVariant() { return variant; } public Object getVariantValue() { return variantValue; } public String getReason() { return reason; } public String getFlagKey() { return flagKey; } // Helper methods public boolean hasVariant() { return variant != null; } public <T> T getVariantValue(Class<T> type) { if (variantValue == null) return null; return type.cast(variantValue); } public static FeatureEvaluationResult disabled(String flagKey) { return new FeatureEvaluationResult(false, null, null, "FEATURE_DISABLED", flagKey); } public static FeatureEvaluationResult enabled(String flagKey, String variant, Object variantValue) { return new FeatureEvaluationResult(true, variant, variantValue, "FEATURE_ENABLED", flagKey); } public static FeatureEvaluationResult enabled(String flagKey) { return new FeatureEvaluationResult(true, null, null, "FEATURE_ENABLED", flagKey); } }

Step 4: Feature Flag Service

Core Feature Flag Service

FeatureFlagService.java

package com.example.featureflags.service; import com.example.featureflags.model.*; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.Optional; @Service public class FeatureFlagService { private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class); private final FeatureFlagStorage storage; private final MeterRegistry meterRegistry; public FeatureFlagService(FeatureFlagStorage storage, MeterRegistry meterRegistry) { this.storage = storage; this.meterRegistry = meterRegistry; } @Cacheable(value = "featureFlags", key = "#key") public Optional<FeatureFlag> getFlag(String key) { logger.debug("Fetching feature flag: {}", key); return storage.getFlag(key); } public FeatureEvaluationResult evaluateFlag(String key, EvaluationContext context) { return evaluateFlag(key, context, null); } public FeatureEvaluationResult evaluateFlag(String key, EvaluationContext context, Map<String, Object> defaultVariants) { long startTime = System.currentTimeMillis(); try { Optional<FeatureFlag> flagOpt = getFlag(key); if (flagOpt.isEmpty()) { // Flag not found, check if we have a default variant if (defaultVariants != null && defaultVariants.containsKey(key)) { Object defaultValue = defaultVariants.get(key); if (defaultValue instanceof Boolean && (Boolean) defaultValue) { return FeatureEvaluationResult.enabled(key); } } return FeatureEvaluationResult.disabled(key); } FeatureFlag flag = flagOpt.get(); // Check if feature is globally disabled if (!flag.getEnabled()) { return FeatureEvaluationResult.disabled(key); } // Evaluate targeting rules in order for (TargetingRule rule : flag.getTargetingRules()) { if (rule.evaluate(context)) { String variant = rule.getVariant() != null ? rule.getVariant() : flag.getDefaultVariant(); Object variantValue = getVariantValue(flag, variant); recordEvaluationMetrics(key, true, variant, "TARGETING_RULE"); return FeatureEvaluationResult.enabled(key, variant, variantValue); } } // Check rollout percentage if (flag.getRolloutPercentage() != null && flag.getRolloutPercentage() > 0) { PercentageRule percentageRule = new PercentageRule(); percentageRule.setPercentage(flag.getRolloutPercentage()); percentageRule.setVariant(flag.getDefaultVariant()); if (percentageRule.evaluate(context)) { Object variantValue = getVariantValue(flag, flag.getDefaultVariant()); recordEvaluationMetrics(key, true, flag.getDefaultVariant(), "ROLLOUT_PERCENTAGE"); return FeatureEvaluationResult.enabled(key, flag.getDefaultVariant(), variantValue); } } // Feature disabled for this user recordEvaluationMetrics(key, false, null, "NOT_IN_TARGET"); return FeatureEvaluationResult.disabled(key); } catch (Exception e) { logger.error("Error evaluating feature flag: {}", key, e); recordEvaluationMetrics(key, false, null, "ERROR"); return FeatureEvaluationResult.disabled(key); } finally { long duration = System.currentTimeMillis() - startTime; meterRegistry.timer("feature_flag.evaluation.time", "flag", key) .record(java.time.Duration.ofMillis(duration)); } } public boolean isEnabled(String key, EvaluationContext context) { return evaluateFlag(key, context).isEnabled(); } public <T> T getVariant(String key, EvaluationContext context, Class<T> type) { return getVariant(key, context, type, null); } public <T> T getVariant(String key, EvaluationContext context, Class<T> type, T defaultValue) { FeatureEvaluationResult result = evaluateFlag(key, context); if (result.isEnabled() && result.hasVariant()) { T value = result.getVariantValue(type); return value != null ? value : defaultValue; } return defaultValue; } private Object getVariantValue(FeatureFlag flag, String variant) { if (flag.getVariants() != null && flag.getVariants().containsKey(variant)) { return flag.getVariants().get(variant); } return null; } private void recordEvaluationMetrics(String flagKey, boolean enabled, String variant, String reason) { meterRegistry.counter("feature_flag.evaluations", "flag", flagKey, "enabled", String.valueOf(enabled), "variant", variant != null ? variant : "none", "reason", reason ).increment(); } // Administrative methods public FeatureFlag createFlag(FeatureFlag flag) { return storage.saveFlag(flag); } public FeatureFlag updateFlag(String key, FeatureFlag flag) { flag.setKey(key); // Ensure key consistency return storage.saveFlag(flag); } public void deleteFlag(String key) { storage.deleteFlag(key); } public List<FeatureFlag> getAllFlags() { return storage.getAllFlags(); } }

Storage Abstraction

FeatureFlagStorage.java

package com.example.featureflags.service; import com.example.featureflags.model.FeatureFlag; import java.util.List; import java.util.Optional; public interface FeatureFlagStorage { Optional<FeatureFlag> getFlag(String key); FeatureFlag saveFlag(FeatureFlag flag); void deleteFlag(String key); List<FeatureFlag> getAllFlags(); }

RedisFeatureFlagStorage.java

package com.example.featureflags.service; import com.example.featureflags.model.FeatureFlag; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; @Repository public class RedisFeatureFlagStorage implements FeatureFlagStorage { private static final Logger logger = LoggerFactory.getLogger(RedisFeatureFlagStorage.class); private final RedisTemplate<String, String> redisTemplate; private final ObjectMapper objectMapper; private final String keyPrefix; public RedisFeatureFlagStorage(RedisTemplate<String, String> redisTemplate, ObjectMapper objectMapper, @Value("${feature.flags.storage.redis.key-prefix:feature_flags:}") String keyPrefix) { this.redisTemplate = redisTemplate; this.objectMapper = objectMapper; this.keyPrefix = keyPrefix; } @Override public Optional<FeatureFlag> getFlag(String key) { try { String redisKey = keyPrefix + key; String json = redisTemplate.opsForValue().get(redisKey); if (json == null) { return Optional.empty(); } FeatureFlag flag = objectMapper.readValue(json, FeatureFlag.class); return Optional.of(flag); } catch (JsonProcessingException e) { logger.error("Error deserializing feature flag: {}", key, e); return Optional.empty(); } } @Override public FeatureFlag saveFlag(FeatureFlag flag) { try { String redisKey = keyPrefix + flag.getKey(); String json = objectMapper.writeValueAsString(flag); redisTemplate.opsForValue().set(redisKey, json); // Also add to index redisTemplate.opsForSet().add(keyPrefix + "index", flag.getKey()); logger.info("Saved feature flag: {}", flag.getKey()); return flag; } catch (JsonProcessingException e) { logger.error("Error serializing feature flag: {}", flag.getKey(), e); throw new RuntimeException("Failed to save feature flag", e); } } @Override public void deleteFlag(String key) { String redisKey = keyPrefix + key; redisTemplate.delete(redisKey); redisTemplate.opsForSet().remove(keyPrefix + "index", key); logger.info("Deleted feature flag: {}", key); } @Override public List<FeatureFlag> getAllFlags() { List<FeatureFlag> flags = new ArrayList<>(); Set<String> flagKeys = redisTemplate.opsForSet().members(keyPrefix + "index"); if (flagKeys == null) { return flags; } for (String key : flagKeys) { getFlag(key).ifPresent(flags::add); } return flags; } }

Step 5: Feature Flag Client

FeatureFlagClient.java

package com.example.featureflags.client; import com.example.featureflags.model.EvaluationContext; import com.example.featureflags.model.FeatureEvaluationResult; import com.example.featureflags.service.FeatureFlagService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class FeatureFlagClient { private static final Logger logger = LoggerFactory.getLogger(FeatureFlagClient.class); private final FeatureFlagService featureFlagService; private final Map<String, Object> defaultVariants; public FeatureFlagClient(FeatureFlagService featureFlagService) { this.featureFlagService = featureFlagService; this.defaultVariants = new ConcurrentHashMap<>(); } public boolean isEnabled(String featureKey) { return isEnabled(featureKey, EvaluationContext.builder().build()); } public boolean isEnabled(String featureKey, String userId) { return isEnabled(featureKey, EvaluationContext.builder().userId(userId).build()); } public boolean isEnabled(String featureKey, EvaluationContext context) { return featureFlagService.isEnabled(featureKey, context); } public FeatureEvaluationResult evaluate(String featureKey, EvaluationContext context) { return featureFlagService.evaluateFlag(featureKey, context, defaultVariants); } public <T> T getVariant(String featureKey, EvaluationContext context, Class<T> type) { return featureFlagService.getVariant(featureKey, context, type); } public <T> T getVariant(String featureKey, EvaluationContext context, Class<T> type, T defaultValue) { return featureFlagService.getVariant(featureKey, context, type, defaultValue); } public void setDefaultVariant(String featureKey, Object defaultValue) { defaultVariants.put(featureKey, defaultValue); } // Convenience methods for common use cases public boolean isEnabledForUser(String featureKey, String userId, Map<String, Object> userProperties) { EvaluationContext context = EvaluationContext.builder() .userId(userId) .userProperties(userProperties) .build(); return isEnabled(featureKey, context); } public String getStringVariant(String featureKey, EvaluationContext context, String defaultValue) { return getVariant(featureKey, context, String.class, defaultValue); } public Integer getIntegerVariant(String featureKey, EvaluationContext context, Integer defaultValue) { return getVariant(featureKey, context, Integer.class, defaultValue); } public Boolean getBooleanVariant(String featureKey, EvaluationContext context, Boolean defaultValue) { return getVariant(featureKey, context, Boolean.class, defaultValue); } public Map<String, Object> getMapVariant(String featureKey, EvaluationContext context, Map<String, Object> defaultValue) { @SuppressWarnings("unchecked") Map<String, Object> result = getVariant(featureKey, context, Map.class, defaultValue); return result != null ? result : defaultValue; } }

Step 6: Spring Configuration

FeatureFlagConfig.java

package com.example.featureflags.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching public class FeatureFlagConfig { @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5)) .disableCachingNullValues(); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } }

Step 7: Usage Examples

1. Basic Feature Toggle

OrderService.java

package com.example.service; import com.example.featureflags.client.FeatureFlagClient; import com.example.featureflags.model.EvaluationContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderService.class); private final FeatureFlagClient featureFlagClient; public OrderService(FeatureFlagClient featureFlagClient) { this.featureFlagClient = featureFlagClient; } public Order processOrder(OrderRequest request) { // Check if new payment processing is enabled for this user EvaluationContext context = EvaluationContext.builder() .userId(request.getUserId()) .userProperty("tier", request.getUserTier()) .userProperty("country", request.getCountry()) .build(); boolean newPaymentEnabled = featureFlagClient.isEnabled( "new-payment-processor", context); PaymentResult paymentResult; if (newPaymentEnabled) { logger.info("Using new payment processor for user: {}", request.getUserId()); paymentResult = newPaymentProcessor.process(request); } else { logger.info("Using legacy payment processor for user: {}", request.getUserId()); paymentResult = legacyPaymentProcessor.process(request); } // Continue with order processing... return createOrder(request, paymentResult); } }

2. A/B Testing with Variants

RecommendationService.java

package com.example.service; import com.example.featureflags.client.FeatureFlagClient; import com.example.featureflags.model.EvaluationContext; import org.springframework.stereotype.Service; import java.util.Map; @Service public class RecommendationService { private final FeatureFlagClient featureFlagClient; public RecommendationService(FeatureFlagClient featureFlagClient) { this.featureFlagClient = featureFlagClient; } public List<Product> getRecommendations(String userId, String sessionId) { EvaluationContext context = EvaluationContext.builder() .userId(userId) .sessionId(sessionId) .userProperty("user_segment", getUserSegment(userId)) .build(); // Get the recommendation algorithm variant String algorithmVariant = featureFlagClient.getStringVariant( "recommendation-algorithm", context, "legacy" // default value ); // Get algorithm parameters Map<String, Object> algorithmParams = featureFlagClient.getMapVariant( "recommendation-params", context, Map.of("maxResults", 10, "diversity", 0.5) ); switch (algorithmVariant) { case "collaborative-filtering": return collaborativeFilteringAlgorithm.recommend(userId, algorithmParams); case "content-based": return contentBasedAlgorithm.recommend(userId, algorithmParams); case "hybrid": return hybridAlgorithm.recommend(userId, algorithmParams); default: return legacyAlgorithm.recommend(userId, algorithmParams); } } public void trackRecommendationPerformance(String userId, String variant, double clickThroughRate) { // Track A/B test results // This data can be used to determine winning variant metricsService.track("recommendation.performance", Map.of("userId", userId, "variant", variant, "ctr", clickThroughRate)); } }

3. Gradual Rollout with Percentage

NewFeatureService.java

package com.example.service; import com.example.featureflags.client.FeatureFlagClient; import com.example.featureflags.model.EvaluationContext; import org.springframework.stereotype.Service; @Service public class NewFeatureService { private final FeatureFlagClient featureFlagClient; public NewFeatureService(FeatureFlagClient featureFlagClient) { this.featureFlagClient = featureFlagClient; } public void launchNewFeature(FeatureRequest request) { String userId = request.getUserId(); EvaluationContext context = EvaluationContext.builder() .userId(userId) .userProperty("account_age_days", getAccountAgeDays(userId)) .userProperty("feature_usage_count", getFeatureUsageCount(userId)) .build(); // Gradually rollout to 10% of users initially boolean newUIFeatureEnabled = featureFlagClient.isEnabled("new-ui-feature", context); if (newUIFeatureEnabled) { renderNewUI(request); // Track adoption metrics metricsService.increment("new_ui.feature.used", Map.of("userId", userId)); } else { renderLegacyUI(request); } } public void trackFeatureUsage(String userId, String featureName, boolean success) { // Track how users interact with the new feature metricsService.track("feature.usage", Map.of("userId", userId, "feature", featureName, "success", success)); } }

4. Operational Feature Flags

SystemService.java

package com.example.service; import com.example.featureflags.client.FeatureFlagClient; import org.springframework.stereotype.Service; @Service public class SystemService { private final FeatureFlagClient featureFlagClient; public SystemService(FeatureFlagClient featureFlagClient) { this.featureFlagClient = featureFlagClient; } public void processBackgroundJob(JobRequest job) { // Kill switch for stopping all background processing if (!featureFlagClient.isEnabled("background-processing-enabled")) { logger.warn("Background processing is disabled via feature flag"); return; } // Circuit breaker for external service if (!featureFlagClient.isEnabled("external-service-circuit-closed")) { throw new ServiceUnavailableException("External service temporarily disabled"); } // Process the job... processJob(job); } public void performMaintenance() { // Check if maintenance mode is enabled if (featureFlagClient.isEnabled("maintenance-mode")) { throw new MaintenanceException("System is under maintenance"); } // Perform maintenance tasks... } }

Step 8: Management API

FeatureFlagController.java

package com.example.featureflags.controller; import com.example.featureflags.model.FeatureFlag; import com.example.featureflags.service.FeatureFlagService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/v1/feature-flags") public class FeatureFlagController { private final FeatureFlagService featureFlagService; public FeatureFlagController(FeatureFlagService featureFlagService) { this.featureFlagService = featureFlagService; } @GetMapping public ResponseEntity<List<FeatureFlag>> getAllFlags() { return ResponseEntity.ok(featureFlagService.getAllFlags()); } @GetMapping("/{key}") public ResponseEntity<FeatureFlag> getFlag(@PathVariable String key) { return featureFlagService.getFlag(key) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<FeatureFlag> createFlag(@RequestBody FeatureFlag flag) { return ResponseEntity.ok(featureFlagService.createFlag(flag)); } @PutMapping("/{key}") public ResponseEntity<FeatureFlag> updateFlag(@PathVariable String key, @RequestBody FeatureFlag flag) { return ResponseEntity.ok(featureFlagService.updateFlag(key, flag)); } @DeleteMapping("/{key}") public ResponseEntity<Void> deleteFlag(@PathVariable String key) { featureFlagService.deleteFlag(key); return ResponseEntity.ok().build(); } @PostMapping("/{key}/enable") public ResponseEntity<FeatureFlag> enableFlag(@PathVariable String key) { return featureFlagService.getFlag(key) .map(flag -> { flag.setEnabled(true); return ResponseEntity.ok(featureFlagService.updateFlag(key, flag)); }) .orElse(ResponseEntity.notFound().build()); } @PostMapping("/{key}/disable") public ResponseEntity<FeatureFlag> disableFlag(@PathVariable String key) { return featureFlagService.getFlag(key) .map(flag -> { flag.setEnabled(false); return ResponseEntity.ok(featureFlagService.updateFlag(key, flag)); }) .orElse(ResponseEntity.notFound().build()); } @PostMapping("/{key}/rollout/{percentage}") public ResponseEntity<FeatureFlag> updateRollout(@PathVariable String key, @PathVariable Double percentage) { return featureFlagService.getFlag(key) .map(flag -> { flag.setRolloutPercentage(percentage); return ResponseEntity.ok(featureFlagService.updateFlag(key, flag)); }) .orElse(ResponseEntity.notFound().build()); } }

Step 9: Sample Feature Flag Definitions

1. Gradual Rollout Flag

{ "key": "new-checkout-experience", "name": "New Checkout Experience", "description": "Gradual rollout of new checkout UI", "enabled": true, "rolloutStrategy": "GRADUAL", "rolloutPercentage": 25.0, "targetingRules": [ { "type": "USER_PROPERTY", "name": "Premium Users", "enabled": true, "property": "user_tier", "value": "premium", "operator": "EQUALS" } ], "variants": { "control": "old_checkout", "treatment": "new_checkout" }, "defaultVariant": "treatment" }

2. A/B Testing Flag

{ "key": "pricing-page-test", "name": "Pricing Page A/B Test", "description": "Test different pricing page layouts", "enabled": true, "rolloutStrategy": "A_B_TESTING", "rolloutPercentage": 100.0, "targetingRules": [ { "type": "PERCENTAGE", "name": "Variant A - 33%", "enabled": true, "percentage": 33.0, "variant": "layout_a" }, { "type": "PERCENTAGE", "name": "Variant B - 33%", "enabled": true, "percentage": 33.0, "variant": "layout_b" }, { "type": "PERCENTAGE", "name": "Variant C - 34%", "enabled": true, "percentage": 34.0, "variant": "layout_c" } ], "variants": { "layout_a": { "layout": "grid", "highlightedTier": "premium", "ctaColor": "blue" }, "layout_b": { "layout": "list", "highlightedTier": "basic", "ctaColor": "green" }, "layout_c": { "layout": "comparison", "highlightedTier": "standard", "ctaColor": "orange" } }, "defaultVariant": "layout_a" }

3. Operational Kill Switch

{ "key": "payment-processing-enabled", "name": "Payment Processing Kill Switch", "description": "Emergency kill switch for payment processing", "enabled": true, "rolloutStrategy": "ALL_OR_NOTHING", "rolloutPercentage": 100.0, "targetingRules": [], "metadata": { "emergency_contact": "[email protected]", "impact_level": "critical" } }

Best Practices

1. Naming Conventions

  • Use consistent, descriptive names: new-feature-rollout, ab-test-pricing
  • Group related flags: payment-new-processor, payment-fallback-mode
  • Avoid environment-specific names in flag definitions

2. Lifecycle Management

  • Regularly clean up unused flags
  • Document flag purpose and ownership
  • Set up expiration policies for temporary flags

3. Monitoring and Alerting

  • Monitor evaluation latency and error rates
  • Alert on rapid changes in flag evaluation patterns
  • Track business metrics alongside flag usage

4. Security

  • Restrict flag modification to authorized users
  • Audit all flag changes
  • Validate flag configurations before deployment

5. Performance

  • Cache flag evaluations appropriately
  • Use efficient data structures for targeting rules
  • Monitor memory usage with large flag sets

Conclusion

This comprehensive dark launch implementation provides:

  • Flexible targeting with multiple rule types (user ID, properties, percentages, cohorts)
  • A/B testing support with variant management
  • Gradual rollouts with percentage-based deployment
  • Operational controls for kill switches and circuit breakers
  • Performance optimization with caching and efficient evaluation
  • Monitoring integration with metrics and analytics

By implementing this dark launch strategy, you can:

  • Reduce deployment risk by gradually exposing new features
  • Gather real-user feedback before full rollout
  • Run experiments to optimize feature performance
  • Quickly respond to issues with operational controls
  • Make data-driven decisions about feature releases

This approach enables safer, more controlled feature deployments while maximizing learning and minimizing risk.

Leave a Reply

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


Macro Nepal Helper