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.