Skip to content

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-checkout is 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 the FLUXGATE_SDK_KEY environment variable.

Packages

ArtifactUse it forLinks
com.forgeopslabs:fluxgate-javaAny 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-starterSpring 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:

pom.xml
<dependency>
  <groupId>com.forgeopslabs</groupId>
  <artifactId>fluxgate-java</artifactId>
  <version>1.0.0</version>
</dependency>
build.gradle.kts
implementation("com.forgeopslabs:fluxgate-java:1.0.0")

Create one client for the whole application and evaluate with a context:

Checkout.java
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());
    }
}
Output
checkout: express
express TARGETING_MATCH null
MethodReturns
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:

pom.xml
<dependency>
  <groupId>com.forgeopslabs</groupId>
  <artifactId>fluxgate-spring-boot-starter</artifactId>
  <version>1.0.0</version>
</dependency>
build.gradle.kts
implementation("com.forgeopslabs:fluxgate-spring-boot-starter:1.0.0")

Set the edge address and the SDK key:

application.yml
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:

CheckoutService.java
@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:

FeatureAdd
OpenFeature providerdev.openfeature:sdk
@FeatureToggleorg.springframework.boot:spring-boot-starter-aop
Metricsio.micrometer:micrometer-core and a MeterRegistry, for example through spring-boot-starter-actuator
Health indicatororg.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:

BannerService.java
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:

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

PropertyDefaultDescription
fluxgate.enabledtrueWhen 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-timeout5sTimeouts for one request.
fluxgate.retry.max-attempts3Attempts per call. Only I/O errors and HTTP 502, 503 and 504 are retried.
fluxgate.retry.initial-backoff100msWait before the first retry, doubled after each retry.
fluxgate.circuit-breaker.failure-threshold5Consecutive transport failures that open the breaker. 0 turns it off.
fluxgate.circuit-breaker.open-duration30sHow long the breaker stays open before one trial call.
fluxgate.cache.enabled, fluxgate.cache.ttl, fluxgate.cache.max-sizetrue, 30s, 1000The cache of successful evaluations.
fluxgate.polling.interval30sHow often the OpenFeature provider checks for flag changes. 0 turns polling off.
fluxgate.openfeature.auto-registerfalseRegisters the provider with OpenFeatureAPI at startup.
fluxgate.health.enabled, fluxgate.metrics.enabledtrueThe 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:

OpenFeature setup
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. errorCode is one of FLAG_NOT_FOUND, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, PROVIDER_NOT_READY, UNAUTHORIZED and GENERAL.
  • Types are exact. An integer flag read with getString returns your default with TYPE_MISMATCH. Integers can be read as Double.
  • 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 evaluateAsync in production, define a FluxGateAsyncExecutor bean, for example FluxGateAsyncExecutor.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.
  • @FeatureToggle works 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.