Skip to main content

Java

A server-side Java SDK for Flagpole feature flags — a polling client on the JDK HttpClient, with Servlet and Spring Boot glue.

Updates are by polling, not streaming

Unlike the Node SDK, the Java SDK does not hold a WebSocket. It refreshes the flag set by calling the REST API on an interval (FlagpoleConfig.pollInterval, default 30 seconds). A flag change therefore takes up to one poll interval to take effect.

Lower the interval for faster propagation, or call client.refresh() after a known change:

FlagpoleConfig.builder().pollInterval(Duration.ofSeconds(5)).build();

Setting the interval to Duration.ZERO disables polling entirely — the flag set is then only what start() loaded plus any manual refresh().

Table of Contents​

Features​

  • ☕ JDK-native transport: built on java.net.http.HttpClient — no OkHttp/Apache
  • ⚡ Non-blocking reads: after start(), isEnabled(...) is a lock-free in-memory lookup
  • 🔁 Polling: configurable-interval REST refresh on a daemon thread
  • 🌱 Spring Boot: auto-configuration, @RequireFeatureFlag, a request filter
  • 🧩 Plain Servlet: FlagpoleFilter / RequireFlagFilter for any container
  • 🌍 Environment Targeting: load and evaluate flags per environment
  • 🔐 Fail-safe: unknown or unreachable flags return your configured fallback

Installation​

// build.gradle.kts
dependencies {
implementation("dev.useflagpole:flagpole-java:0.0.1")
}
<dependency>
<groupId>dev.useflagpole</groupId>
<artifactId>flagpole-java</artifactId>
<version>0.0.1</version>
</dependency>

Requirements​

  • Java 17+
  • jackson-databind (transitive). Servlet / Spring glue activates only if those APIs are already on your classpath.

Quick Start​

1. Create and start the client​

import dev.useflagpole.FlagpoleClient;
import dev.useflagpole.FlagpoleConfig;
import java.util.List;

FlagpoleClient client = new FlagpoleClient(
"fp_live_your_api_key",
FlagpoleConfig.builder()
.environments(List.of("production"))
.fallback("new-checkout", false)
.build());

client.start();

start() performs the first fetch and then polls in the background. It throws a FlagpoleException if that first fetch fails, but the client stays usable and keeps polling — reads fall back until a later fetch succeeds.

2. Check flags​

if (client.isEnabled("new-checkout")) {
// ...
}

Optional<FeatureFlag> flag = client.flag("new-checkout");
Map<String, FeatureFlag> all = client.allFlags();

3. Shut down​

client.close();   // stops polling; FlagpoleClient is AutoCloseable

API Reference​

FlagpoleClient​

MemberDescription
new FlagpoleClient(apiKey) / (apiKey, config)Construct. Performs no I/O.
void start()Initial fetch, then start polling. Re-fetches if called again.
void refresh()Re-fetch every flag now
boolean isEnabled(String)Fallback for unknown / disabled / non-matching flags. Never throws.
Optional<FeatureFlag> flag(String)The full flag
Map<String, FeatureFlag> allFlags()A snapshot of every loaded flag
boolean isStarted()Whether start() has completed once
void close()Stop polling (AutoCloseable)

FlagpoleConfig.builder()​

MethodDefaultDescription
environment(FlagpoleEnvironment)PRODUCTIONWhich backend to talk to
environments(List<String>)null (all)Flag environments to load and evaluate against
pollInterval(Duration)30sRefresh frequency. Duration.ZERO disables polling.
requestTimeout(Duration)10sPer-request timeout
fallback(String, boolean) / fallbacks(Map)–Value for a flag that isn't loaded
baseUrlOverride(String)–Replace the host derived from environment

FeatureFlag​

A record: id(), name(), description(), isEnabled() (raw switch — prefer client.isEnabled(name)), project(), organization(), conditions() (Map<String, Object>), environments() (List<String>, empty means "all"), createdAt() / updatedAt() (raw ISO-8601 strings).

FlagpoleException​

A sealed unchecked exception: FlagpoleException.Api (has statusCode(), body()), FlagpoleException.InvalidResponse, FlagpoleException.Transport.

Spring Boot​

Add the dependency, set your key, and the auto-configuration wires a FlagpoleClient bean (calling start() for you — a failed first fetch just logs and the app still boots), a FlagpoleFilter, and enforcement of @RequireFeatureFlag.

flagpole:
api-key: fp_live_your_api_key
environments: [production]
poll-interval: 30s
request-timeout: 10s
fallbacks:
new-checkout: false
@RestController
class CheckoutController {

private final FlagpoleClient flagpole;

CheckoutController(FlagpoleClient flagpole) {
this.flagpole = flagpole;
}

@GetMapping("/checkout")
String checkout() {
return flagpole.isEnabled("new-checkout") ? "new" : "legacy";
}

@GetMapping("/beta")
@RequireFeatureFlag("beta-access") // 404 when off
String beta() {
return "welcome";
}

@GetMapping("/early")
@RequireFeatureFlag(value = "early-access", disabledStatus = 403)
String early() {
return "welcome";
}
}

@RequireFeatureFlag also works on a @RestController class to gate every method.

Plain Servlet​

FlagpoleClient client = new FlagpoleClient("fp_live_your_api_key");
client.start();

// attach the client to every request
context.addFilter("flagpole", new FlagpoleFilter(client))
.addMappingForUrlPatterns(null, false, "/*");

// gate a path
context.addFilter("beta-gate", new RequireFlagFilter(client, "beta-access", 404))
.addMappingForUrlPatterns(null, false, "/beta/*");

Downstream:

FlagpoleClient flagpole = FlagpoleFilter.fromRequest(request).orElseThrow();
boolean on = flagpole.isEnabled("new-checkout");

Configuration​

Environments​

environments is a filter — it controls which flags are loaded and how isEnabled evaluates targeting. A flag with no environments always applies.

Backend host​

FlagpoleEnvironmentREST
DEVELOPMENThttp://localhost:5000
STAGINGhttps://api.staging.useflagpole.dev
PRODUCTIONhttps://useflagpole-api.onrender.com

Logging​

The SDK logs through System.Logger under the name dev.useflagpole, so it routes to whatever logging backend your app configures (SLF4J, Log4j2, JUL).

Error Handling​

isEnabled(...) returns the configured fallback (default false) for any flag it can't resolve and never throws.

start() and refresh() throw an unchecked FlagpoleException:

try {
client.start();
} catch (FlagpoleException.Api e) {
log.warn("Flagpole rejected the request: {} {}", e.statusCode(), e.body());
// carry on — isEnabled will use fallbacks
}

A failed poll is logged and retried on the next tick; it does not stop the client.

Best Practices​

1. One client per process​

Create it at startup (or let Spring do it), share it, close() on shutdown.

2. Start before serving traffic​

Call start() in your bootstrap so the first request already has flags.

3. Tune the poll interval​

30s suits most rollouts. Drop it to a few seconds for kill-switch flags, or call refresh() from an admin webhook.

4. Always set fallbacks for critical flags​

FlagpoleConfig.builder().fallback("payments-v2", false).build();

Troubleshooting​

Flags are stale​

Expected up to one poll interval. Lower pollInterval or call refresh().

Flags are always the fallback value​

  • Wrong or expired API key
  • The flag targets environments not in your environments list
  • The first fetch failed — check the logs / catch the FlagpoleException from start()

The Spring auto-configuration didn't kick in​

It is gated on flagpole.api-key being set and Spring MVC (DispatcherServlet) being on the classpath.

Contributing​

The SDK lives in the flagpole-sdks monorepo under packages/server/java.

git clone https://github.com/flagpole-corp/flagpole-sdks.git
cd flagpole-sdks/packages/server/java

gradle assemble
gradle test
gradle spotlessCheck

License​

MIT

Support​