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
- Toggle Lifetime Management
- Regularly clean up old feature toggles
- Use naming conventions (e.g.,
feat-*,kill-*,ops-*) - Document toggle purpose and ownership
- Monitoring and Observability
- Log toggle evaluations for debugging
- Track toggle usage metrics
- Set up alerts for critical kill switches
- Security
- Restrict toggle modification permissions
- Validate toggle configurations
- Audit toggle changes
- 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.