Building a Feature Toggle System in Spring Boot for Safe Production Deployments

Building a Feature Toggle System in Spring Boot for Safe Production Deployments

Intended Reader and Concrete Outcome

This guide targets mid-to-senior Java developers and software engineers working with Spring Boot 3.x and Java 17+ who want to implement a dynamic feature toggle system for runtime control of feature availability in production.

After completing this tutorial, you will have a database-backed, thread-safe feature toggle system with a REST API for toggle management, in-memory caching for performance, and practical usage demonstrated through code examples and verification steps.

Prerequisites and Version Assumptions

  • Java 17 or higher
  • Spring Boot 3.x (tested with Spring Data JPA, Spring Web, optionally Spring Security)
  • Maven or Gradle build systems
  • Basic understanding of REST APIs, concurrency, and Spring annotations

Why and When to Use Feature Toggles

Feature toggles are runtime flags controlling whether certain features are active or inactive without requiring application restarts or redeployment. They are critical in:

  • Production safety: Quickly disable malfunctioning features.
  • Gradual rollout: Expose features to subsets of users for testing (canary releases, A/B testing).
  • Decoupling release from deployment: Deploy code without immediately exposing features.

When to Use

  • When your deployment cycles are decoupled from business releases.
  • For operational flexibility to toggle features without pipeline delays.
  • When guarding risky or experimental functionality.

When Not to Use

  • For configurations better handled through environment variables or config management.
  • As a permanent conditional compilation or heavy branching tool — toggles ideally have a limited lifecycle.

Alternatives

  • Feature branches and scheduled releases.
  • External services like LaunchDarkly or Unleash for enterprise-level requirements.
  • Dark launching techniques with traffic shadowing.

Trade-offs

  • Adds complexity and potential technical debt if toggles proliferate.
  • Requires operational discipline: Monitoring, auditing, and cleanup.
  • Performance overhead if toggle checks are not cached or optimized.

Architectural Overview and Design Decisions

We will build:

  • Persistence: A relational database-backed store (H2 demo) for durability.
  • Caching: An in-memory concurrent cache to minimize DB hits and maintain fast reads.
  • API: A RESTful service to query and update toggles dynamically.
  • Usage: Integration in business logic for conditional feature enabling.

This approach offers dynamic updates without app restarts and supports distributed systems with some caveats about cache invalidation.

Storage Choices

Storage TypeProsConsUse Case
In-memoryVery fast, simpleLost on restart, not sharedPrototyping, testing
Relational DBPersistent, easy queryingLatency, requires cachingProduction with moderate scale
External service (SAAS)Scalable, centralized controlCost, dependency, complexityLarge-scale distributed systems

End-to-End Implementation

We use Spring Boot, H2 database, and Spring Data JPA for the demo. Replace H2 with production-grade DB in live use.

1. Project Setup

Initialize with dependencies: web, data-jpa, h2.

Use Spring Initializr or CLI:

spring init --dependencies=web,data-jpa,h2 feature-toggle-demo

2. Define the FeatureToggle Entity

Each toggle has a unique name and an enabled flag.

package com.example.featuretoggle.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class FeatureToggle {

    @Id
    private String name;
    private boolean enabled;

    public FeatureToggle() {}

    public FeatureToggle(String name, boolean enabled) {
        this.name = name;
        this.enabled = enabled;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

3. Create the Repository Interface

package com.example.featuretoggle.repository;

import com.example.featuretoggle.entity.FeatureToggle;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FeatureToggleRepository extends JpaRepository<FeatureToggle, String> {
}

4. Implement FeatureToggleService with Caching

We load toggles at application startup, maintain a concurrent hash map to cache toggle states for fast access, and update both DB and cache on changes.

package com.example.featuretoggle.service;

import com.example.featuretoggle.entity.FeatureToggle;
import com.example.featuretoggle.repository.FeatureToggleRepository;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class FeatureToggleService {

    private final FeatureToggleRepository repository;
    private final Map<String, Boolean> toggleCache = new ConcurrentHashMap<>();

    public FeatureToggleService(FeatureToggleRepository repository) {
        this.repository = repository;
    }

    @PostConstruct
    public void loadToggles() {
        List<FeatureToggle> toggles = repository.findAll();
        toggles.forEach(toggle -> toggleCache.put(toggle.getName(), toggle.isEnabled()));
    }

    public boolean isFeatureEnabled(String featureName) {
        return toggleCache.getOrDefault(featureName, false);
    }

    public synchronized void setFeatureState(String featureName, boolean enabled) {
        FeatureToggle toggle = repository.findById(featureName)
            .orElse(new FeatureToggle(featureName, enabled));
        toggle.setEnabled(enabled);
        repository.save(toggle);
        toggleCache.put(featureName, enabled);
    }

    public void reloadToggles() {
        toggleCache.clear();
        loadToggles();
    }

    public Map<String, Boolean> getAllToggles() {
        return Map.copyOf(toggleCache);
    }
}

Explanation

  • loadToggles loads all toggle states into an in-memory concurrent map for O(1) reads.
  • isFeatureEnabled quickly returns toggle state or false by default.
  • setFeatureState serializes update to DB and cache to keep consistent.
  • reloadToggles supports manual cache refresh.

5. REST Controller for Runtime Toggle Management

The REST API enables querying and modifying toggles at runtime.

package com.example.featuretoggle.controller;

import com.example.featuretoggle.service.FeatureToggleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/features")
public class FeatureToggleController {

    private final FeatureToggleService toggleService;

    public FeatureToggleController(FeatureToggleService toggleService) {
        this.toggleService = toggleService;
    }

    @GetMapping("/{featureName}")
    public ResponseEntity<Boolean> getFeatureState(@PathVariable String featureName) {
        boolean enabled = toggleService.isFeatureEnabled(featureName);
        return ResponseEntity.ok(enabled);
    }

    @PostMapping("/{featureName}")
    public ResponseEntity<String> setFeatureState(@PathVariable String featureName,
                                                  @RequestParam boolean enabled) {
        toggleService.setFeatureState(featureName, enabled);
        return ResponseEntity.ok(String.format("Feature '%s' set to %b", featureName, enabled));
    }

    @GetMapping
    public Map<String, Boolean> getAllToggles() {
        return toggleService.getAllToggles();
    }
}

6. Using Feature Toggles in Business Logic

Conditionally enable feature code paths based on toggle states.

package com.example.featuretoggle.service;

import org.springframework.stereotype.Component;

@Component
public class SomeBusinessService {

    private final FeatureToggleService toggleService;

    public SomeBusinessService(FeatureToggleService toggleService) {
        this.toggleService = toggleService;
    }

    public String performBusinessLogic() {
        if (toggleService.isFeatureEnabled("newFeature")) {
            return newFeatureImplementation();
        } else {
            return legacyImplementation();
        }
    }

    private String newFeatureImplementation() {
        return "New Feature Enabled";
    }

    private String legacyImplementation() {
        return "Legacy Feature";
    }
}

Code Interaction Explanation

  • The SomeBusinessService queries FeatureToggleService on each action.
  • Reads hit the local in-memory cache, ensuring minimal performance impact.

Verification Steps

  1. Start the application.
  1. Check toggle default (should be false):
curl http://localhost:8080/api/features/newFeature

Expected: false

  1. Enable the feature:
curl -X POST "http://localhost:8080/api/features/newFeature?enabled=true"

Expected: Feature &#39;newFeature&#39; set to true

  1. Verify toggle state again:
curl http://localhost:8080/api/features/newFeature

Expected: true

  1. Test application behavior:
  • Trigger SomeBusinessService.performBusinessLogic() (e.g., via a test or REST endpoint).
  • Verify output is "New Feature Enabled" when toggle is true and "Legacy Feature" when false.

Production Failure Modes and Troubleshooting

  • Cache Out-of-Sync: If the cache does not refresh after external DB changes,
  • Mitigation: Add scheduled reloads or implement push notifications/event listeners when toggle data changes.
  • Database Latency: Excessive direct DB queries on toggle states can impair latency.
  • Mitigation: Rely on the in-memory cache exclusively for reads.
  • Unauthorized Access: Toggle management endpoints exposed without auth can lead to security breaches.
  • Mitigation: Apply security layers (OAuth2, JWT, Basic Auth) and RBAC.
  • Stale Toggles Accumulation: Long-lived toggles increase complexity.
  • Mitigation: Governance policies on toggle lifecycle and audits.
  • Consistency in Multi-instance Environments: Because toggles are cached locally on each instance, updates may not propagate immediately.
  • Mitigation: Utilize distributed caches (e.g., Redis) or event-driven invalidation.

Security Considerations

To avoid risks:

  • Secure toggle management REST endpoints via Spring Security.
  • Implement user authentication and enforce role-based access control for management APIs.
  • Log all toggle changes with user, timestamp, and IP for audit trails.

Example minimal Spring Security config snippet:

// Configure HTTP Basic Auth for sensitive endpoints
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/features/**").hasRole("ADMIN")
            .anyRequest().permitAll()
            .and().httpBasic();
    }
}

Performance Considerations

  • Use a thread-safe cache (ConcurrentHashMap) for quick reads.
  • Avoid DB queries on every feature check.
  • Optimize setFeatureState to synchronize writes and keep cache consistent.
  • For very high throughput, consider distributed caches or reactive approaches.

Operational Safeguards

  • Monitor toggle service uptime and REST API responsiveness.
  • Implement logging for toggle query and modification actions.
  • Provide an emergency global kill-switch (e.g., a master toggle) to disable all experimental features.
  • Automate periodic cleanup of obsolete toggles.

Limitations

  • This example uses H2 in-memory DB for demonstration — in production use a reliable RDBMS.
  • Cache invalidation is manual; concurrent toggle changes from outside require a reload mechanism.
  • No UI is included; consider integrating or building a web dashboard.
  • No user-level targeting or segmentation; used toggles are simple boolean flags.

Summary

A reliable, dynamic feature toggle system in Spring Boot involves:

  • Persisting toggle states in a database
  • Employing a thread-safe in-memory cache for low-latency reads
  • Providing a secure REST API for runtime toggle management
  • Integrating toggle checks in application logic for feature gating

While this sample fits moderate scale and simple toggles, advanced needs may require external flag services, richer targeting, distributed caching, and UI management tools.


FAQ

What is the main advantage of using database-backed toggles?

Persistence ensures toggles survive restarts and remain consistent across multiple application instances, allowing dynamic updates without downtime.

How can I secure my feature toggle APIs?

Use Spring Security to enforce authentication and role-based authorization, limiting toggle modifications to trusted administrators.

How do I prevent accumulating technical debt from toggles?

Regularly audit and purge toggles that are no longer needed and avoid using toggles as permanent configuration or feature management.

Can feature toggles impact application performance?

Yes, frequent database checks without caching can cause latency. Using an in-memory cache minimizes overhead and provides near-instant evaluation.

When should I consider using external feature flag services?

For complex feature targeting, audit trails, centralized management across teams, or multi-application environments, external SaaS providers offer mature solutions.


Sources and further reading


Related reading