Dynamic Feature Control: Implementing Robust Feature Toggles in Java Applications

Feature toggles (or feature flags) are a powerful technique that allows you to modify system behavior without changing code. They enable gradual rollouts, A/B testing, emergency kill switches, and environment-specific functionality. Let's explore how to implement robust feature toggles in Java applications using various configuration strategies.

Architecture Overview

Feature Toggle Config → Toggle Manager → Feature Service → Application Code
↑
(Dynamic Sources:
File, Database,
ConfigMap, Redis)

Strategy 1: Spring Boot Configuration-Based Toggles

Dependencies

<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Basic Toggle Configuration

// src/main/java/com/company/feature/FeatureConfig.java
@Configuration
@ConfigurationProperties(prefix = "features")
@Data
public class FeatureConfig {
private Map<String, Boolean> toggles = new HashMap<>();
private Map<String, PercentageToggle> percentageToggles = new HashMap<>();
private Map<String, String> variantToggles = new HashMap<>();
@Data
public static class PercentageToggle {
private int percentage;
private boolean defaultWhenDisabled = false;
}
}

Application Properties

# application.yml
features:
toggles:
new-payment-gateway: true
advanced-reporting: false
dark-mode: true
social-login: false
percentage-toggles:
beta-feature:
percentage: 10
default-when-disabled: false
gradual-rollout:
percentage: 50
default-when-disabled: true
variant-toggles:
theme-color: "blue"
pricing-tier: "premium"
api-version: "v2"
management:
endpoints:
web:
exposure:
include: features,health,info

Strategy 2: Advanced Feature Toggle Service

Core Feature Toggle Interface

// src/main/java/com/company/feature/FeatureToggleService.java
public interface FeatureToggleService {
boolean isEnabled(String featureName);
boolean isEnabled(String featureName, String userId);
String getVariant(String featureName);
String getVariant(String featureName, String userId);
void refreshToggles();
Map<String, Object> getAllToggles();
}

Implementation with Refreshable Configuration

// src/main/java/com/company/feature/ConfigFeatureToggleService.java
@Service
@Slf4j
public class ConfigFeatureToggleService implements FeatureToggleService {
private final FeatureConfig featureConfig;
private final ObjectMapper objectMapper;
private final Random random;
// Cache for user-based consistent decisions
private final Map<String, Map<String, Boolean>> userFeatureCache = new ConcurrentHashMap<>();
public ConfigFeatureToggleService(FeatureConfig featureConfig, 
ObjectMapper objectMapper) {
this.featureConfig = featureConfig;
this.objectMapper = objectMapper;
this.random = new Random();
}
@Override
public boolean isEnabled(String featureName) {
return isEnabled(featureName, null);
}
@Override
public boolean isEnabled(String featureName, String userId) {
// Check simple boolean toggles first
Boolean simpleToggle = featureConfig.getToggles().get(featureName);
if (simpleToggle != null) {
return simpleToggle;
}
// Check percentage-based toggles
FeatureConfig.PercentageToggle percentageToggle = 
featureConfig.getPercentageToggles().get(featureName);
if (percentageToggle != null) {
return isPercentageEnabled(featureName, percentageToggle, userId);
}
log.warn("Feature toggle not found: {}", featureName);
return false;
}
private boolean isPercentageEnabled(String featureName, 
FeatureConfig.PercentageToggle toggle, 
String userId) {
if (userId != null) {
// Consistent behavior for same user
return getUserPercentageDecision(featureName, toggle, userId);
} else {
// Random decision for anonymous users
return random.nextInt(100) < toggle.getPercentage();
}
}
private boolean getUserPercentageDecision(String featureName, 
FeatureConfig.PercentageToggle toggle,
String userId) {
return userFeatureCache
.computeIfAbsent(userId, k -> new HashMap<>())
.computeIfAbsent(featureName, k -> {
// Deterministic based on user ID
int hash = Math.abs(userId.hashCode());
return (hash % 100) < toggle.getPercentage();
});
}
@Override
public String getVariant(String featureName) {
return getVariant(featureName, null);
}
@Override
public String getVariant(String featureName, String userId) {
String variant = featureConfig.getVariantToggles().get(featureName);
return variant != null ? variant : "default";
}
@Override
public void refreshToggles() {
userFeatureCache.clear();
log.info("Feature toggle cache cleared");
}
@Override
public Map<String, Object> getAllToggles() {
Map<String, Object> allToggles = new HashMap<>();
allToggles.put("booleanToggles", featureConfig.getToggles());
allToggles.put("percentageToggles", featureConfig.getPercentageToggles());
allToggles.put("variantToggles", featureConfig.getVariantToggles());
return allToggles;
}
}

Strategy 3: Dynamic Feature Toggles with Database Backend

Database Schema

-- feature_toggles.sql
CREATE TABLE feature_toggles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
enabled BOOLEAN DEFAULT FALSE,
percentage INT DEFAULT 0,
variant VARCHAR(100),
target_users JSON,
start_time TIMESTAMP NULL,
end_time TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE feature_toggle_audit (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
feature_name VARCHAR(255) NOT NULL,
user_id VARCHAR(255),
enabled BOOLEAN,
accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

JPA Entity and Repository

// src/main/java/com/company/feature/entity/FeatureToggle.java
@Entity
@Table(name = "feature_toggles")
@Data
@EntityListeners(AuditingEntityListener.class)
public class FeatureToggle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private Boolean enabled = false;
private Integer percentage = 0;
private String variant;
@Column(columnDefinition = "JSON")
private String targetUsers; // JSON array of user IDs
private Instant startTime;
private Instant endTime;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@Transient
public List<String> getTargetUserList() {
if (targetUsers == null || targetUsers.trim().isEmpty()) {
return Collections.emptyList();
}
try {
return Arrays.asList(new ObjectMapper().readValue(targetUsers, String[].class));
} catch (Exception e) {
return Collections.emptyList();
}
}
}
// Repository
@Repository
public interface FeatureToggleRepository extends JpaRepository<FeatureToggle, Long> {
Optional<FeatureToggle> findByName(String name);
List<FeatureToggle> findByEnabledTrue();
}

Advanced Database Feature Service

// src/main/java/com/company/feature/DatabaseFeatureToggleService.java
@Service
@Slf4j
public class DatabaseFeatureToggleService implements FeatureToggleService {
private final FeatureToggleRepository repository;
private final FeatureToggleAuditRepository auditRepository;
private final ObjectMapper objectMapper;
// Local cache with TTL
private final Cache<String, FeatureToggle> toggleCache = 
Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
public DatabaseFeatureToggleService(FeatureToggleRepository repository,
FeatureToggleAuditRepository auditRepository,
ObjectMapper objectMapper) {
this.repository = repository;
this.auditRepository = auditRepository;
this.objectMapper = objectMapper;
}
@Override
public boolean isEnabled(String featureName) {
return isEnabled(featureName, null);
}
@Override
public boolean isEnabled(String featureName, String userId) {
FeatureToggle toggle = getToggle(featureName);
if (toggle == null) {
log.warn("Feature toggle not found: {}", featureName);
auditAccess(featureName, userId, false);
return false;
}
boolean enabled = evaluateToggle(toggle, userId);
auditAccess(featureName, userId, enabled);
return enabled;
}
private FeatureToggle getToggle(String featureName) {
return toggleCache.get(featureName, key -> 
repository.findByName(key).orElse(null)
);
}
private boolean evaluateToggle(FeatureToggle toggle, String userId) {
// Check time window
if (!isWithinTimeWindow(toggle)) {
return false;
}
// Check targeted users
if (userId != null && !toggle.getTargetUserList().isEmpty()) {
return toggle.getTargetUserList().contains(userId);
}
// Check percentage rollout
if (toggle.getPercentage() > 0 && userId != null) {
return isUserInPercentage(toggle.getName(), userId, toggle.getPercentage());
}
// Simple boolean toggle
return Boolean.TRUE.equals(toggle.getEnabled());
}
private boolean isWithinTimeWindow(FeatureToggle toggle) {
Instant now = Instant.now();
return (toggle.getStartTime() == null || !now.isBefore(toggle.getStartTime())) &&
(toggle.getEndTime() == null || !now.isAfter(toggle.getEndTime()));
}
private boolean isUserInPercentage(String featureName, String userId, int percentage) {
// Consistent hashing for same user
int hash = Math.abs((featureName + ":" + userId).hashCode());
return (hash % 100) < percentage;
}
private void auditAccess(String featureName, String userId, boolean enabled) {
try {
FeatureToggleAudit audit = new FeatureToggleAudit();
audit.setFeatureName(featureName);
audit.setUserId(userId);
audit.setEnabled(enabled);
auditRepository.save(audit);
} catch (Exception e) {
log.warn("Failed to audit feature toggle access", e);
}
}
@Override
public void refreshToggles() {
toggleCache.invalidateAll();
log.info("Feature toggle cache refreshed");
}
@Scheduled(fixedRate = 60000) // Refresh every minute
public void scheduledRefresh() {
refreshToggles();
}
}

Strategy 4: Kubernetes ConfigMap-Based Feature Toggles

ConfigMap Definition

# k8s/feature-toggles-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feature-toggles
labels:
app: java-app
data:
features.json: |
{
"toggles": {
"new-ui": true,
"beta-features": false,
"payment-v2": true
},
"percentageToggles": {
"gradual-migration": {
"percentage": 25,
"defaultWhenDisabled": true
}
},
"variants": {
"theme": "dark",
"api-version": "v2"
}
}

ConfigMap Watcher Integration

// src/main/java/com/company/feature/ConfigMapFeatureManager.java
@Component
@Slf4j
public class ConfigMapFeatureManager {
private final FeatureToggleService featureService;
private final ObjectMapper objectMapper;
private volatile String currentConfigHash;
public ConfigMapFeatureManager(FeatureToggleService featureService,
ObjectMapper objectMapper) {
this.featureService = featureService;
this.objectMapper = objectMapper;
}
@EventListener
public void onConfigMapUpdate(ConfigMapUpdateEvent event) {
if (event.getConfigData().containsKey("features.json")) {
try {
String newConfig = event.getConfigData().get("features.json");
String newHash = calculateHash(newConfig);
if (!newHash.equals(currentConfigHash)) {
updateFeatures(newConfig);
currentConfigHash = newHash;
log.info("Feature toggles updated from ConfigMap");
}
} catch (Exception e) {
log.error("Failed to update features from ConfigMap", e);
}
}
}
private void updateFeatures(String configJson) throws Exception {
FeatureConfig newConfig = objectMapper.readValue(configJson, FeatureConfig.class);
// In a real implementation, you would update the feature service
featureService.refreshToggles();
}
private String calculateHash(String content) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(content.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
return content; // Fallback to content comparison
}
}
}

Strategy 5: Feature Toggle Controllers and Management

REST API for Feature Management

// src/main/java/com/company/feature/FeatureToggleController.java
@RestController
@RequestMapping("/api/features")
@Validated
@Slf4j
public class FeatureToggleController {
private final FeatureToggleService featureService;
public FeatureToggleController(FeatureToggleService featureService) {
this.featureService = featureService;
}
@GetMapping
public Map<String, Object> getAllFeatures() {
return featureService.getAllToggles();
}
@GetMapping("/{featureName}/enabled")
public boolean isFeatureEnabled(@PathVariable String featureName,
@RequestParam(required = false) String userId) {
return featureService.isEnabled(featureName, userId);
}
@GetMapping("/{featureName}/variant")
public String getFeatureVariant(@PathVariable String featureName,
@RequestParam(required = false) String userId) {
return featureService.getVariant(featureName, userId);
}
@PostMapping("/refresh")
public ResponseEntity<Void> refreshFeatures() {
featureService.refreshToggles();
return ResponseEntity.ok().build();
}
}

Spring Boot Actuator Endpoint

// src/main/java/com/company/feature/FeatureToggleEndpoint.java
@Component
@Endpoint(id = "features")
@Slf4j
public class FeatureToggleEndpoint {
private final FeatureToggleService featureService;
public FeatureToggleEndpoint(FeatureToggleService featureService) {
this.featureService = featureService;
}
@ReadOperation
public Map<String, Object> features() {
return featureService.getAllToggles();
}
@WriteOperation
public void refresh() {
featureService.refreshToggles();
}
}

Practical Usage Examples

1. Gradual Feature Rollout

// src/main/java/com/company/service/PaymentService.java
@Service
@Slf4j
public class PaymentService {
private final FeatureToggleService featureToggle;
private final PaymentGatewayV1 gatewayV1;
private final PaymentGatewayV2 gatewayV2;
public PaymentService(FeatureToggleService featureToggle,
PaymentGatewayV1 gatewayV1,
PaymentGatewayV2 gatewayV2) {
this.featureToggle = featureToggle;
this.gatewayV1 = gatewayV1;
this.gatewayV2 = gatewayV2;
}
public PaymentResult processPayment(PaymentRequest request, String userId) {
if (featureToggle.isEnabled("payment-gateway-v2", userId)) {
log.info("Using payment gateway V2 for user: {}", userId);
return gatewayV2.process(request);
} else {
log.info("Using payment gateway V1 for user: {}", userId);
return gatewayV1.process(request);
}
}
}

2. A/B Testing

// src/main/java/com/company/service/RecommendationService.java
@Service
public class RecommendationService {
private final FeatureToggleService featureToggle;
private final AlgorithmA algorithmA;
private final AlgorithmB algorithmB;
public List<Product> getRecommendations(String userId) {
String variant = featureToggle.getVariant("recommendation-algorithm", userId);
switch (variant) {
case "algorithm-a":
return algorithmA.getRecommendations(userId);
case "algorithm-b":
return algorithmB.getRecommendations(userId);
default:
return algorithmA.getRecommendations(userId);
}
}
public void trackRecommendationPerformance(String userId, String productId, boolean purchased) {
String variant = featureToggle.getVariant("recommendation-algorithm", userId);
// Send to analytics for A/B test evaluation
analyticsService.trackConversion(variant, userId, productId, purchased);
}
}

3. Emergency Kill Switch

// src/main/java/com/company/service/DataExportService.java
@Service
public class DataExportService {
private final FeatureToggleService featureToggle;
@Async
public CompletableFuture<ExportResult> exportUserData(String userId) {
// Check kill switch before starting expensive operation
if (!featureToggle.isEnabled("data-export-enabled")) {
throw new FeatureDisabledException("Data export feature is currently disabled");
}
return CompletableFuture.supplyAsync(() -> {
// Perform expensive data export
return performExport(userId);
});
}
}

Testing Feature Toggles

Unit Tests

// src/test/java/com/company/feature/FeatureToggleServiceTest.java
@ExtendWith(MockitoExtension.class)
class FeatureToggleServiceTest {
@Mock
private FeatureConfig featureConfig;
@InjectMocks
private ConfigFeatureToggleService featureService;
@Test
void testFeatureEnabled() {
// Given
Map<String, Boolean> toggles = new HashMap<>();
toggles.put("new-feature", true);
when(featureConfig.getToggles()).thenReturn(toggles);
// When
boolean enabled = featureService.isEnabled("new-feature");
// Then
assertThat(enabled).isTrue();
}
@Test
void testPercentageToggleConsistency() {
// Given
String userId = "user-123";
FeatureConfig.PercentageToggle toggle = new FeatureConfig.PercentageToggle();
toggle.setPercentage(50);
Map<String, FeatureConfig.PercentageToggle> percentageToggles = new HashMap<>();
percentageToggles.put("gradual-rollout", toggle);
when(featureConfig.getPercentageToggles()).thenReturn(percentageToggles);
// When - multiple calls should return same result
boolean firstCall = featureService.isEnabled("gradual-rollout", userId);
boolean secondCall = featureService.isEnabled("gradual-rollout", userId);
// Then
assertThat(firstCall).isEqualTo(secondCall);
}
}

Integration Test

// src/test/java/com/company/feature/FeatureToggleIT.java
@SpringBootTest
@TestPropertySource(properties = {
"features.toggles.new-ui=true",
"features.percentage-toggles.beta-feature.percentage=25"
})
class FeatureToggleIT {
@Autowired
private FeatureToggleService featureService;
@Test
void testFeatureToggleIntegration() {
assertThat(featureService.isEnabled("new-ui")).isTrue();
assertThat(featureService.isEnabled("non-existent")).isFalse();
}
}

Best Practices

  1. Toggle Lifetime Management
  • Regularly clean up old feature toggles
  • Use naming conventions (e.g., feat-*, kill-*, ops-*)
  • Document toggle purpose and ownership
  1. Monitoring and Observability
  • Log toggle evaluations for debugging
  • Track toggle usage metrics
  • Set up alerts for critical kill switches
  1. Security
  • Restrict toggle modification permissions
  • Validate toggle configurations
  • Audit toggle changes
  1. Performance
  • Cache toggle states appropriately
  • Use efficient data structures for user-based toggles
  • Minimize network calls for remote toggle services

Conclusion

Feature toggles are essential for modern Java applications, enabling:

  • Safe deployments with gradual rollouts
  • A/B testing and data-driven decisions
  • Operational control with kill switches
  • Environment-specific behavior without code changes

Choose the right strategy based on your needs:

  • Configuration-based: Simple, suitable for most applications
  • Database-backed: Dynamic, great for runtime management
  • External services: Enterprise-grade, full-featured

By implementing robust feature toggles, you gain unprecedented control over your application's behavior while maintaining stability and enabling rapid innovation.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

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


Macro Nepal Helper