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.
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.
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.
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
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.
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
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.