Reference
Java and Spring Boot SDK
Add fluxgate-java to any Java 17 app, or the Spring Boot starter to configure it from properties and guard methods with @FeatureToggle.
Before you start
- Java 17 or newer. The starter needs Spring Boot 3.5.
- The edge server is running and
express-checkoutis deployed to Development. See Clients and edge server. - The SDK key of a Backend client, in the story
checkout-service. Keep it out of source control, for example in theFLUXGATE_SDK_KEYenvironment variable.
Packages
| Artifact | Use it for | Links |
|---|---|---|
com.forgeopslabs:fluxgate-java | Any Java 17 app. The typed FluxGateClient (JDK HttpClient, cache, retries, circuit breaker) and the OpenFeature provider FluxGateProvider. No Spring. | Maven Central, Javadoc |
com.forgeopslabs:fluxgate-spring-boot-starter | Spring Boot 3.5 apps. Configures fluxgate-java from fluxgate.* properties and adds @FeatureToggle, Micrometer metrics and an Actuator health indicator. | Maven Central |
Source code, samples and the full reference: forgeopslabs/fluxgate-springboot on GitHub. The current version is 1.0.0.
Plain Java
Add the client to your build:
<dependency>
<groupId>com.forgeopslabs</groupId>
<artifactId>fluxgate-java</artifactId>
<version>1.0.0</version>
</dependency>implementation("com.forgeopslabs:fluxgate-java:1.0.0")Create one client for the whole application and evaluate with a context:
import com.forgeopslabs.fluxgate.DefaultFluxGateClient;
import com.forgeopslabs.fluxgate.EvaluationContext;
import com.forgeopslabs.fluxgate.FlagEvaluation;
import com.forgeopslabs.fluxgate.FluxGateClient;
import com.forgeopslabs.fluxgate.FluxGateOptions;
public class Checkout {
public static void main(String[] args) {
FluxGateClient flags = DefaultFluxGateClient.create(FluxGateOptions.builder()
.baseUrl("http://localhost:8081")
.sdkKey(System.getenv("FLUXGATE_SDK_KEY")) // <clientId>.<apiKey>
.build());
EvaluationContext user = EvaluationContext.builder()
.targetingKey("user-1")
.attribute("country", "CA")
.attribute("user_tier", "plus")
.build();
String variant = flags.getString("express-checkout", "classic", user);
System.out.println("checkout: " + variant);
FlagEvaluation<String> details = flags.evaluate("express-checkout", "classic", String.class, user);
System.out.println(details.variant() + " " + details.reason() + " " + details.errorCode());
}
}checkout: express
express TARGETING_MATCH null| Method | Returns |
|---|---|
isEnabled(key, ctx), getBoolean(key, default, ctx) | boolean |
getString, getInteger, getDouble, getObject (a Map) | The value, or your default |
evaluate(key, default, Class<T>, ctx) | FlagEvaluation<T>: value, variant, reason, errorCode, errorMessage, metadata |
evaluateAsync(key, default, Class<T>, ctx) | CompletableFuture<FlagEvaluation<T>>, which always completes normally |
bulkEvaluate(ctx) | Every flag for the context, revalidated with ETag and If-None-Match |
FluxGateOptions sets the same timeouts, retries, cache and circuit breaker as the starter properties below, with the same defaults.
Spring Boot
Add the starter:
<dependency>
<groupId>com.forgeopslabs</groupId>
<artifactId>fluxgate-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>implementation("com.forgeopslabs:fluxgate-spring-boot-starter:1.0.0")Set the edge address and the SDK key:
fluxgate:
base-url: http://localhost:8081
sdk-key: ${FLUXGATE_SDK_KEY}Both are required. If one is missing or malformed, startup fails with a message that names the property, never the key itself. Then inject FluxGateClient:
@Service
class CheckoutService {
private final FluxGateClient flags;
CheckoutService(FluxGateClient flags) {
this.flags = flags;
}
String checkoutVariant(Customer customer) {
EvaluationContext context = EvaluationContext.builder()
.targetingKey(customer.id())
.attribute("country", customer.country())
.attribute("user_tier", customer.tier())
.build();
return flags.getString("express-checkout", "classic", context);
}
}Optional features
Each feature turns on when its dependency is on the classpath:
| Feature | Add |
|---|---|
| OpenFeature provider | dev.openfeature:sdk |
@FeatureToggle | org.springframework.boot:spring-boot-starter-aop |
| Metrics | io.micrometer:micrometer-core and a MeterRegistry, for example through spring-boot-starter-actuator |
| Health indicator | org.springframework.boot:spring-boot-starter-actuator |
Guard a method with @FeatureToggle
The annotation runs a method only while its flag is on. While it is off, or when the evaluation fails, the fallback method runs with the same arguments:
import com.forgeopslabs.fluxgate.spring.annotation.FeatureToggle;
@Service
public class BannerService {
@FeatureToggle(value = "holiday-banner", fallbackMethod = "noBanner")
public String banner(String userId, String country) {
return "Holiday sale: free shipping";
}
private String noBanner(String userId, String country) {
return "";
}
}The annotation needs to know who the caller is. Declare an EvaluationContextResolver bean that builds the context, here from the method arguments:
import com.forgeopslabs.fluxgate.EvaluationContext;
import com.forgeopslabs.fluxgate.spring.annotation.EvaluationContextResolver;
@Configuration
class FlagConfiguration {
@Bean
EvaluationContextResolver evaluationContextResolver() {
return (flagKey, method, arguments) -> EvaluationContext.builder()
.targetingKey((String) arguments[0])
.attribute("country", arguments[1])
.build();
}
}Without a resolver the context has no targeting key, so every annotated flag counts as off. A fallback must take exactly the same parameter types and return a compatible type. If it does not, startup fails. Without a fallback, an off flag returns null, Optional.empty(), or false or 0 for primitives.
Properties
| Property | Default | Description |
|---|---|---|
fluxgate.enabled | true | When false, nothing contacts the edge and every flag returns your default. |
fluxgate.base-url | (required) | Edge server URL. |
fluxgate.sdk-key | (required) | SDK key, <clientId>.<apiKey>. |
fluxgate.connect-timeout, fluxgate.read-timeout | 5s | Timeouts for one request. |
fluxgate.retry.max-attempts | 3 | Attempts per call. Only I/O errors and HTTP 502, 503 and 504 are retried. |
fluxgate.retry.initial-backoff | 100ms | Wait before the first retry, doubled after each retry. |
fluxgate.circuit-breaker.failure-threshold | 5 | Consecutive transport failures that open the breaker. 0 turns it off. |
fluxgate.circuit-breaker.open-duration | 30s | How long the breaker stays open before one trial call. |
fluxgate.cache.enabled, fluxgate.cache.ttl, fluxgate.cache.max-size | true, 30s, 1000 | The cache of successful evaluations. |
fluxgate.polling.interval | 30s | How often the OpenFeature provider checks for flag changes. 0 turns polling off. |
fluxgate.openfeature.auto-register | false | Registers the provider with OpenFeatureAPI at startup. |
fluxgate.health.enabled, fluxgate.metrics.enabled | true | The health indicator and the Micrometer metrics. |
The jar ships configuration metadata, so your IDE completes these properties.
Metrics and health
With Micrometer, the starter records the timer fluxgate.evaluation (tags flag, reason, variant, error) and the counter fluxgate.cache (tag result, hit or miss). Targeting keys and context attributes are never tags.
With Actuator, the health indicator fluxgate appears at /actuator/health/fluxgate. It answers from cached state and never calls the edge during a health request.
OpenFeature
Add dev.openfeature:sdk and register the provider. With plain Java, wrap your client:
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(new FluxGateProvider(flags));
Client client = api.getClient();
MutableContext context = new MutableContext("user-1");
context.add("country", "CA");
context.add("user_tier", "plus");
String variant = client.getStringValue("express-checkout", "classic", context);With Spring Boot, the starter creates a FluxGateProvider bean. Register it yourself, or set fluxgate.openfeature.auto-register: true to register it at startup without blocking.
The provider polls the flag set every polling.interval with If-None-Match. When flags change, it clears the cache and emits PROVIDER_CONFIGURATION_CHANGED with the changed flag keys. A failed poll moves it to ERROR; the next successful poll brings it back to READY.
Good to know
- Evaluations never throw.
errorCodeis one ofFLAG_NOT_FOUND,TYPE_MISMATCH,TARGETING_KEY_MISSING,INVALID_CONTEXT,PROVIDER_NOT_READY,UNAUTHORIZEDandGENERAL. - Types are exact. An integer flag read with
getStringreturns your default withTYPE_MISMATCH. Integers can be read asDouble. - The shortcuts without a context, such as
isEnabled(key), have no targeting key, so they always return your default. Pass a context. - Evaluations are blocking HTTP calls. With the defaults, an edge that does not answer can hold one call for about 30 seconds: 3 attempts of 5 s connect and 5 s read, plus backoff. Lower the timeouts if your request path cannot wait that long. After 5 failures in a row, the circuit breaker answers at once with your default and
PROVIDER_NOT_READY. - For
evaluateAsyncin production, define aFluxGateAsyncExecutorbean, for exampleFluxGateAsyncExecutor.of(Executors.newVirtualThreadPerTaskExecutor())on Java 21. Otherwise the calls run on the common fork-join pool. - A flag change can take up to
cache.ttl(30 s) to show up, unless the OpenFeature provider is registered with polling on. Then a new flag version clears the cache. @FeatureToggleworks only through Spring proxies: calls from the same class, and private, final or static methods, are not intercepted.
Try the samples
The repository has a plain Java sample and a Spring Boot sample. samples/docker-compose.yml starts a separate FluxGate stack on ports 18000 (admin UI), 18080 (backend) and 18081 (edge), and samples/setup.sh creates a Backend client, a boolean flag and a contextual flag. Follow samples/README.md.
Next
For the other languages, see TypeScript, React and Angular and Rust. The HTTP calls behind every SDK are in the Edge server API.