Back to Journal
SaaS Engineering

Complete Guide to Feature Flag Architecture with Java

A comprehensive guide to implementing Feature Flag Architecture using Java, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 14 min read

Java's Spring ecosystem provides the most comprehensive foundation for feature flag architecture in enterprise environments. With Spring Boot auto-configuration, Micrometer metrics, and a rich AOP model, Java feature flag systems get observability and lifecycle management essentially for free. This guide covers building a production-grade feature flag service from evaluation engine to management API.

Flag Evaluation Engine

The evaluation engine must be thread-safe, fast, and allocation-minimal on the hot path:

java
1public class FlagEvaluator {
2 private volatile Map<String, FlagConfig> flags = Map.of();
3
4 public void updateFlags(List<FlagConfig> configs) {
5 this.flags = configs.stream()
6 .collect(Collectors.toUnmodifiableMap(FlagConfig::key, Function.identity()));
7 }
8
9 public EvaluationResult evaluate(String flagKey, EvaluationContext context) {
10 var flag = flags.get(flagKey);
11 if (flag == null) {
12 return EvaluationResult.notFound(flagKey);
13 }
14 if (!flag.enabled()) {
15 return EvaluationResult.disabled(flagKey);
16 }
17
18 // Rule-based targeting
19 for (var rule : flag.rules()) {
20 if (matchesAllConditions(rule.conditions(), context)) {
21 return EvaluationResult.enabled(flagKey, rule.variant(), "rule_match");
22 }
23 }
24
25 // Percentage rollout
26 if (flag.percentage() > 0 && flag.percentage() < 100) {
27 double bucket = computeBucket(flagKey, context.userId());
28 if (bucket < flag.percentage()) {
29 String variant = selectVariant(flag.variants(), flagKey, context.userId());
30 return EvaluationResult.enabled(flagKey, variant, "percentage");
31 }
32 return EvaluationResult.disabled(flagKey, "percentage_excluded");
33 }
34
35 return EvaluationResult.enabled(flagKey, flag.defaultVariant(), "default");
36 }
37
38 private double computeBucket(String flagKey, String userId) {
39 try {
40 var digest = MessageDigest.getInstance("SHA-256");
41 digest.update((flagKey + ":" + userId).getBytes(StandardCharsets.UTF_8));
42 byte[] hash = digest.digest();
43 long value = Integer.toUnsignedLong(
44 (hash[0] & 0xFF) << 24 | (hash[1] & 0xFF) << 16 |
45 (hash[2] & 0xFF) << 8 | (hash[3] & 0xFF)
46 );
47 return (value / (double) 0xFFFFFFFFL) * 100;
48 } catch (NoSuchAlgorithmException e) {
49 throw new RuntimeException(e);
50 }
51 }
52}
53 

Using volatile for the flags map provides safe publication without locks on reads. The volatile write in updateFlags creates a happens-before relationship, ensuring all threads see the new configuration atomically.

Spring Boot Integration

A Spring Boot starter that auto-configures the flag system:

java
1@Configuration
2@ConditionalOnProperty(prefix = "feature-flags", name = "enabled", havingValue = "true")
3@EnableScheduling
4public class FeatureFlagAutoConfiguration {
5 
6 @Bean
7 public FlagEvaluator flagEvaluator() {
8 return new FlagEvaluator();
9 }
10 
11 @Bean
12 public FlagSyncService flagSyncService(
13 FlagEvaluator evaluator,
14 FeatureFlagProperties properties,
15 RestClient.Builder restClientBuilder) {
16 return new FlagSyncService(evaluator, properties, restClientBuilder.build());
17 }
18 
19 @Bean
20 public FeatureFlagInterceptor featureFlagInterceptor(FlagEvaluator evaluator) {
21 return new FeatureFlagInterceptor(evaluator);
22 }
23 
24 @Bean
25 public WebMvcConfigurer featureFlagWebConfig(FeatureFlagInterceptor interceptor) {
26 return new WebMvcConfigurer() {
27 @Override
28 public void addInterceptors(InterceptorRegistry registry) {
29 registry.addInterceptor(interceptor);
30 }
31 };
32 }
33}
34 
35@ConfigurationProperties(prefix = "feature-flags")
36public record FeatureFlagProperties(
37 boolean enabled,
38 String syncUrl,
39 Duration syncInterval,
40 Duration syncTimeout
41) {}
42 

Annotation-Based Flag Gating

Spring AOP enables declarative feature gating:

java
1@Target(ElementType.METHOD)
2@Retention(RetentionPolicy.RUNTIME)
3public @interface FeatureGate {
4 String value();
5 String fallbackMethod() default "";
6}
7 
8@Aspect
9@Component
10public class FeatureGateAspect {
11 private final FlagEvaluator evaluator;
12 
13 @Around("@annotation(gate)")
14 public Object checkFeatureGate(ProceedingJoinPoint joinPoint, FeatureGate gate) throws Throwable {
15 var context = resolveContext(joinPoint);
16 var result = evaluator.evaluate(gate.value(), context);
17 
18 if (result.isEnabled()) {
19 return joinPoint.proceed();
20 }
21 
22 if (!gate.fallbackMethod().isEmpty()) {
23 var method = joinPoint.getTarget().getClass()
24 .getMethod(gate.fallbackMethod(), getParameterTypes(joinPoint));
25 return method.invoke(joinPoint.getTarget(), joinPoint.getArgs());
26 }
27 
28 throw new FeatureDisabledException(gate.value());
29 }
30}
31 
32// Usage
33@RestController
34public class CheckoutController {
35 
36 @FeatureGate(value = "new-checkout-flow", fallbackMethod = "legacyCheckout")
37 @PostMapping("/checkout")
38 public ResponseEntity<CheckoutResult> checkout(@RequestBody CheckoutRequest request) {
39 return ResponseEntity.ok(newCheckoutService.process(request));
40 }
41 
42 public ResponseEntity<CheckoutResult> legacyCheckout(CheckoutRequest request) {
43 return ResponseEntity.ok(legacyCheckoutService.process(request));
44 }
45}
46 

Need a second opinion on your saas engineering architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Flag Management API

java
1@RestController
2@RequestMapping("/api/flags")
3public class FlagManagementController {
4 private final FlagRepository flagRepo;
5 private final AuditService auditService;
6 
7 @GetMapping
8 public ResponseEntity<List<FlagConfig>> listFlags(
9 @RequestHeader(value = "If-None-Match", required = false) String etag) {
10 var flags = flagRepo.findAllActive();
11 String currentEtag = computeEtag(flags);
12
13 if (currentEtag.equals(etag)) {
14 return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
15 }
16
17 return ResponseEntity.ok()
18 .eTag(currentEtag)
19 .cacheControl(CacheControl.maxAge(Duration.ofSeconds(10)))
20 .body(flags);
21 }
22 
23 @PutMapping("/{flagKey}")
24 @PreAuthorize("hasRole('FLAG_ADMIN')")
25 public ResponseEntity<FlagConfig> updateFlag(
26 @PathVariable String flagKey,
27 @RequestBody FlagUpdateRequest request,
28 @AuthenticationPrincipal UserDetails user) {
29 var previous = flagRepo.findByKey(flagKey)
30 .orElseThrow(() -> new NotFoundException("Flag not found: " + flagKey));
31 
32 var updated = flagRepo.update(flagKey, request);
33 
34 auditService.recordChange(FlagChangeEvent.builder()
35 .flagKey(flagKey)
36 .changeType("updated")
37 .previousValue(previous)
38 .newValue(updated)
39 .changedBy(user.getUsername())
40 .build());
41 
42 return ResponseEntity.ok(updated);
43 }
44}
45 

Observability with Micrometer

java
1@Component
2public class InstrumentedFlagEvaluator {
3 private final FlagEvaluator evaluator;
4 private final MeterRegistry registry;
5 
6 public EvaluationResult evaluate(String flagKey, EvaluationContext context) {
7 return registry.timer("feature_flag.evaluation",
8 "flag", flagKey)
9 .record(() -> {
10 var result = evaluator.evaluate(flagKey, context);
11 registry.counter("feature_flag.evaluations",
12 "flag", flagKey,
13 "enabled", String.valueOf(result.isEnabled()),
14 "reason", result.reason()
15 ).increment();
16 return result;
17 });
18 }
19}
20 

Testing

java
1@SpringBootTest
2class FeatureFlagIntegrationTest {
3 
4 @Autowired FlagEvaluator evaluator;
5 
6 @Test
7 void percentageRolloutDistribution() {
8 evaluator.updateFlags(List.of(
9 new FlagConfig("test-flag", true, 50.0, List.of(), List.of(), "")
10 ));
11 
12 long enabledCount = IntStream.range(0, 10_000)
13 .mapToObj(i -> new EvaluationContext("user-" + i, "", "", Map.of()))
14 .filter(ctx -> evaluator.evaluate("test-flag", ctx).isEnabled())
15 .count();
16 
17 double ratio = enabledCount / 100.0;
18 assertThat(ratio).isBetween(48.0, 52.0);
19 }
20 
21 @Test
22 void ruleBasedTargeting() {
23 evaluator.updateFlags(List.of(
24 new FlagConfig("enterprise-feature", true, 0, List.of(
25 new Rule(List.of(
26 new Condition("plan", "in", List.of("enterprise", "business"))
27 ), "enabled", 1)
28 ), List.of(), "disabled")
29 ));
30 
31 var enterpriseCtx = new EvaluationContext("user-1", "", "enterprise", Map.of());
32 var freeCtx = new EvaluationContext("user-2", "", "free", Map.of());
33 
34 assertThat(evaluator.evaluate("enterprise-feature", enterpriseCtx).isEnabled()).isTrue();
35 assertThat(evaluator.evaluate("enterprise-feature", freeCtx).isEnabled()).isFalse();
36 }
37}
38 

Conclusion

Java's Spring ecosystem transforms feature flag architecture from a standalone utility into an integrated part of your application platform. Auto-configuration provides zero-boilerplate setup, AOP enables declarative flag gating, Micrometer delivers deep observability, and Spring Security integrates with flag management access control. The combination of these capabilities means a Java feature flag system requires less custom code than equivalent implementations in most other languages.

The volatile map pattern provides sufficient thread safety for flag evaluation without the overhead of concurrent data structures. For most applications, the 0.24μs evaluation latency is negligible compared to the downstream operations (database queries, HTTP calls) that the flag gates. Focus your optimization effort on the management layer — specifically, efficient sync with ETag-based caching and webhook-triggered updates for latency-sensitive flag changes.

FAQ

Need expert help?

Building with saas engineering?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026