Skip to main content

Spring Integration

Use one application-scoped TogglyClient, then register the adapter for your application's HTTP stack. Context identifies this request's user; a feature gate selects available behavior and does not replace authentication or authorization.

Spring Boot Auto-Configuration​

This setup uses Toggly 1.5.1, Spring Boot 4.1.1 and Spring Framework 7.0.9. In your Maven POM, import the Spring BOM before the Boot BOM to keep the host's Spring dependencies aligned. Add these sections to an existing Boot application using Java 17 or higher; use its usual Boot build plugin and entry point.

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>7.0.9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>4.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.toggly</groupId>
<artifactId>toggly-spring-boot-starter</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
</dependencies>

Set your application key in TOGGLY_APP_KEY, then place this in src/main/resources/application.yml:

toggly:
app-key: ${TOGGLY_APP_KEY}
environment: Production
refresh-interval-seconds: 30

Put this class under your application's component-scanned package. The explicit provider is necessary on this host: the starter's default provider factory returns null, which Spring cannot inject into the client bean. Spring owns the supplied HTTP provider and closes it at shutdown. The starter owns the client bean.

FeatureConfiguration.java
package example;

import io.toggly.core.TogglyClient;
import io.toggly.core.config.TogglyConfig;
import io.toggly.core.snapshot.HttpSnapshotProvider;
import io.toggly.spring.boot.FeatureAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class FeatureConfiguration {
@Bean(destroyMethod = "close")
public HttpSnapshotProvider snapshotProvider(TogglyConfig config) {
return new HttpSnapshotProvider(config);
}

@Bean
public FeatureAspect featureAspect(TogglyClient client) {
return new FeatureAspect(client);
}
}

The client does not guarantee a downloaded snapshot at construction. An empty snapshot can cause synchronous HTTP I/O during evaluation. Use a startup client.refresh() attempt if appropriate for your application, and configure error reporting on a custom TogglyConfig bean. If the download fails, undefined flags use defaults; successful refreshes replace the definitions used by subsequent evaluations.

Method-Level Feature Gating​

Add this Spring-managed service beside the configuration. FeatureAspect is a native SDK aspect, but it needs the explicit bean above; @Aspect alone does not register it. Boot 4's AOP starter is spring-boot-starter-aspectj.

CheckoutService.java
package example;

import io.toggly.spring.boot.FeatureEnabled;
import org.springframework.stereotype.Service;

@Service
public class CheckoutService {
@FeatureEnabled(value = "new-checkout", fallbackMethod = "legacy")
public String checkout(String orderId) {
return "new:" + orderId;
}

public String legacy(String orderId) {
return "legacy:" + orderId;
}
}

Inject CheckoutService into a controller or another bean and call checkout. With new-checkout ON it returns new:...; with the flag OFF it calls legacy. The fallback must be public with matching parameters. Calls must pass through the Spring proxy: calling an annotated method on this bypasses the aspect. Without a fallback, disabled object-returning methods return null; they do not produce an HTTP 404. Use the MVC gate below when an HTTP denial is intended.

@ConditionalOnFeature is a separate startup-time bean condition. Use TogglyClient or the aspect for behavior that must change while the app is running; a conditional bean is not recreated when definitions refresh.

Spring MVC Integration​

Run the Java Spring MVC sample to explore native interceptor ordering, controller gates, boolean argument injection and request-aware views. Its README includes a dashboard setup recipe and a source-reading map; the sample uses Spring MVC directly with Tomcat 11 and FreeMarker templates.

Add io.toggly:toggly-spring-mvc:1.5.1 and the host's org.springframework.boot:spring-boot-starter-webmvc dependency for a Boot MVC application. In a non-Boot Spring application, provide a managed core client and configuration yourself.

Context, gate and parameter registration​

Use this single configuration, alongside FeatureConfiguration. It installs the context interceptor before the gate and registers the native boolean argument resolver. RequestContexts is the complete helper in Mapping HTTP headers; put it in the same example package. It maps authenticated identity/claims and request segment data.

WebConfig.java
package example;

import io.toggly.core.TogglyClient;
import io.toggly.spring.mvc.FeatureArgumentResolver;
import io.toggly.spring.mvc.FeatureGateInterceptor;
import io.toggly.spring.mvc.TogglyContextInterceptor;
import io.toggly.spring.mvc.TogglyModelAttribute;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
private final TogglyClient client;

public WebConfig(TogglyClient client) {
this.client = client;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TogglyContextInterceptor(RequestContexts::from))
.order(0);
registry.addInterceptor(new FeatureGateInterceptor(client)).order(1);
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new FeatureArgumentResolver(client));
}

@Bean
public TogglyModelAttribute togglyModelAttribute() {
return new TogglyModelAttribute(client);
}
}

If you only need identity/groups from a trusted proxy, replace RequestContexts::from with new HeaderContextResolver("X-User-Id", "X-User-Groups") from io.toggly.spring.mvc. The proxy must remove client-supplied values and set its own authenticated values. That resolver populates only identity and groups, not claims, request segments or an entity. SecurityContextResolver is another native option for principal identity; forSpringSecurityRoles() maps authorities to groups, not claims.

Controller Annotations​

DashboardController.java
package example;

import io.toggly.spring.mvc.FeatureArgumentResolver;
import io.toggly.spring.mvc.FeatureGate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DashboardController {
@GetMapping("/beta-feature")
@FeatureGate("beta-feature")
public String betaFeature() {
return "Beta feature!";
}

@GetMapping("/dashboard")
public String dashboard(
@FeatureArgumentResolver.FeatureFlag("new-dashboard") boolean useNew) {
return useNew ? "dashboard-v2" : "dashboard-v1";
}
}

Start the app with mvn spring-boot:run. Visit /dashboard: it returns dashboard-v1 when new-dashboard is OFF or absent, and dashboard-v2 when ON. /beta-feature returns 404 while its flag is OFF and the response text when ON. Use @FeatureGate(value = "beta-feature", status = 403) to choose a different blocked status. Class-level gates cover the controller's handler methods.

The context interceptor clears its ThreadLocal on synchronous request completion, including errors. For executor work or Servlet async dispatch, pass the immutable context explicitly; this interceptor is not an async context propagation mechanism.

Thymeleaf Integration​

The TogglyModelAttribute bean above adds a features map for each controller request. In a normal Thymeleaf @Controller view, put this in its HTML template (the host needs its Thymeleaf integration and view resolver):

<div th:if="${features['new-header']}">New header</div>
<div th:unless="${features['new-header']}">Old header</div>

A @RestController returns response content, not a template view. With a view-rendering controller, toggle new-header to switch which element renders. The map is evaluated with this request's context.

Spring WebFlux Integration​

For a reactive application, add io.toggly:toggly-spring-webflux:1.5.1 and the host's spring-boot-starter-webflux instead of its MVC starter. Reuse the managed core provider/client setup above. Do not install the MVC interceptors in WebFlux.

Run the Spring WebFlux sample for a complete Reactor Netty host, request and Order contexts, native gates, and a dashboard setup recipe.

Context Filter and Reactive Feature Gates​

Register the context filter before the gate; neither native filter defines its own order. This example uses identity/group headers supplied by a trusted proxy. If your authentication is held elsewhere, implement a ReactiveContextResolver that builds an EvaluationContext from that authenticated principal and the request. It must emit a context, even for an anonymous request, rather than an empty Mono. Built-in header resolution does not populate claims/request/entity.

ReactiveFeatureConfiguration.java
package example;

import io.toggly.core.TogglyClient;
import io.toggly.spring.webflux.FeatureGateFilter;
import io.toggly.spring.webflux.HeaderReactiveContextResolver;
import io.toggly.spring.webflux.ReactiveTogglyClient;
import io.toggly.spring.webflux.TogglyContextFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.WebFilter;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@Configuration
public class ReactiveFeatureConfiguration {
@Bean(destroyMethod = "")
public ReactiveTogglyClient reactiveTogglyClient(TogglyClient client) {
return new ReactiveTogglyClient(client); // Core bean owns client shutdown.
}

@Bean
@Order(-20)
public TogglyContextFilter togglyContextFilter() {
return new TogglyContextFilter(
new HeaderReactiveContextResolver("X-User-Id", "X-User-Groups"));
}

@Bean
@Order(-10)
public WebFilter betaApiFilter(TogglyClient client) {
PathPattern api = PathPatternParser.defaultInstance.parse("/api/v2/**");
FeatureGateFilter gate = FeatureGateFilter.builder(client)
.features("beta-api")
.pathMatcher(exchange -> api.matches(
exchange.getRequest().getPath().pathWithinApplication()))
.blockedStatus(HttpStatus.NOT_FOUND)
.build();
// Application scheduling around the native gate's synchronous evaluation.
return (exchange, chain) -> Mono.defer(() -> gate.filter(exchange, chain))
.subscribeOn(Schedulers.boundedElastic());
}
}

Compile the Spring PathPattern once and pass its predicate to the native pathMatcher. Matching pathWithinApplication() follows Spring controller routing for decoded segments and matrix parameters while preserving encoded slashes inside a segment. The SDK's pathPattern helper compares raw path text and can miss equivalent routes or include neighboring prefixes.

Reactor context carries each subscriber's identity across the scheduler boundary; ThreadLocal does not. The wrapper's evaluation methods call the synchronous core. An empty snapshot can therefore block on HTTP. The gate above offloads that work and its downstream subscription to boundedElastic. A startup refresh can warm the snapshot, but failed or empty downloads still require this consideration.

Reactive Controller Example​

ReactiveController.java
package example;

import io.toggly.spring.webflux.ReactiveTogglyClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@RestController
public class ReactiveController {
private final ReactiveTogglyClient toggly;

public ReactiveController(ReactiveTogglyClient toggly) {
this.toggly = toggly;
}

@GetMapping("/api/data")
public Mono<String> getData() {
return toggly.isEnabled("new-data-api")
.subscribeOn(Schedulers.boundedElastic())
.map(enabled -> enabled ? "new data" : "legacy data");
}

@GetMapping("/api/v2/data")
public Mono<String> gatedData() {
return Mono.just("beta data");
}
}

Visit /api/data to observe ON/OFF selection. /api/v2/data is denied with 404 unless beta-api is ON for the request context. When adapting this to real data services, defer side effects until subscription and select the intended publisher; creating both publishers eagerly can run application code before the flag check.

Actuator Integration​

Toggly 1.5.1's native Actuator auto-configuration is incompatible with Boot 4.1.1: it refers to the old org.springframework.boot.actuate.health.HealthIndicator package. Adding Boot 4 Actuator causes startup to fail while loading that configuration. Setting management.health.toggly.enabled=false does not avoid this class-loading failure.

For a Boot 4 application that uses other Actuator features, exclude Toggly's Actuator auto-configuration in application.yml:

spring:
autoconfigure:
exclude:
- io.toggly.spring.boot.actuator.TogglyActuatorAutoConfiguration

This disables both the native Toggly health indicator and /actuator/toggly endpoint. Core client configuration and the explicitly registered aspect still work. The exclusion does not supply replacement Toggly Actuator functionality. Use your application's operational checks to observe download errors and freshness; a flag count alone does not prove a successful recent signed download.