Feature Flags in Java: The Missing Piece That Makes Trunk-Based Development Work
A practical look at implementing release, ops, and experiment flags with Togglz and Unleash on Spring Boot 4 and the cleanup discipline that keeps them from becoming debt.
Trunk-based development sounds simple on a whiteboard: everyone commits to main, releases happen often, and long-lived branches disappear. In practice, that only works if you have a way to merge unfinished code safely. That mechanism is the feature flag. JCG has already covered the strategic case for trunk-based development; this piece fills the gap that strategy talk usually skips — how to actually wire flags into a Java service without turning your codebase into a maze of forgotten if statements.
Why Trunk-Based Development Needs Flags
Without flags, a team practicing trunk-based development has two uncomfortable options: finish every feature in a single commit, or hide incomplete work behind ad-hoc configuration that nobody tracks. Neither scales. Feature flags decouple deployment from release — the code ships to production behind a switch that stays off until the feature is ready. Martin Fowler’s original write-up on this pattern remains the clearest framing of why the technique works, and it’s worth revisiting even if you’ve read it before, because most teams only remember half of it: the part about turning features on, not the part about turning them off again.
The Three Flag Types You’ll Actually Use
Not every flag serves the same purpose, and treating them all the same is where most flag systems go wrong. Fowler’s taxonomy splits flags by how long they live and who ultimately owns removing them.
| Flag Type | Purpose | Typical Lifespan | Owns Cleanup |
|---|---|---|---|
| Release | Ship incomplete or risky code paths safely, merge to trunk before the feature is finished | Days to a few weeks | Feature team |
| Ops | Runtime kill switch for degrading or disabling a system behavior under load or incident | Indefinite, by design | Platform / SRE team |
| Experiment | A/B or multivariate testing, routing users into cohorts | Weeks to a couple of months | Product / growth team |
The lifespan column matters more than it looks. Release flags are meant to disappear almost as soon as the rollout finishes; ops flags are meant to stay. Confusing the two is exactly how a temporary switch turns into permanent, unreadable branching logic.

Implementing Release Flags with Togglz in Spring Boot 4
Togglz is the lighter of the two options: no external server required, configuration lives in your app, and it plugs into Spring Boot 4.1 through its own starter. It suits release flags well, since those are short-lived and usually don’t need cross-service coordination. A feature is just an enum implementing Togglz’s Feature interface, and the starter wires up a FeatureManager bean automatically.
// build.gradle.kts
implementation("org.togglz:togglz-spring-boot-starter:4.4.0")
implementation("org.togglz:togglz-console-spring-boot-starter:4.4.0")
public enum AppFeatures implements Feature {
@Label("New checkout flow")
NEW_CHECKOUT_FLOW;
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}
if (AppFeatures.NEW_CHECKOUT_FLOW.isActive()) {
// new code path
} else {
// existing behavior
}
Register the enum with togglz.feature-enums=com.example.AppFeatures in application.properties, and the bundled admin console (via togglz-console-spring-boot-starter) gives you a UI to flip the switch without redeploying. Full setup details are in the official Togglz Spring Boot starter docs.
Implementing Flags with Unleash for Bigger Teams
Unleash is a different shape of tool: a standalone feature management platform (open source or hosted) that many services talk to over an API, with built-in gradual rollout strategies and a shared source of truth. It’s the better fit for ops and experiment flags, where multiple services or environments need to agree on the same flag state, and where product teams want to change rollout percentages without a code deploy.
<dependency>
<groupId>io.getunleash</groupId>
<artifactId>unleash-client-java</artifactId>
<version>12.2.2</version>
</dependency>
UnleashConfig config = UnleashConfig.builder()
.appName("checkout-service")
.instanceId("checkout-service-1")
.unleashAPI("https://unleash.example.com/api")
.apiKey("client-api-token")
.build();
Unleash unleash = new DefaultUnleash(config);
if (unleash.isEnabled("new-checkout-flow")) {
// new code path
}
Wrap the Unleash instance in a Spring @Bean and inject it wherever needed; the client polls for updates in the background, so flag changes propagate without restarting the app. The Java SDK documentation covers context providers, which are useful once you want to target flags by user attributes rather than a global on/off.
Togglz vs. Unleash: Picking the Right Tool
These two aren’t really competitors — many teams run both, using Togglz for quick release toggles and Unleash for anything that needs cross-service state or gradual rollout percentages.
| Consideration | Togglz | Unleash |
|---|---|---|
| Deployment model | Embedded in the app, no separate server | Standalone server (self-hosted or cloud) |
| Best suited to | Release flags, single-service toggles | Ops and experiment flags, multi-service state |
| Gradual rollout % | Manual strategy implementation | Built-in rollout strategies |
| Admin UI | Optional console add-on | Full management dashboard included |
| Learning curve | Low — enum and a starter dependency | Moderate — server setup and API tokens |
Cleanup Discipline: The Part Everyone Skips
Flags are cheap to add and easy to forget. That gap is exactly where technical debt accumulates. Stripe’s Developer Coefficient study, based on a survey of more than a thousand developers, found that engineers lose roughly a third of their working week to technical debt and maintenance rather than new development — and forgotten flags are a direct, avoidable contributor to that number.

The consequences of skipping cleanup aren’t theoretical. Uber’s mobile teams found that inactive toggles were bloating their apps and slowing users down, which is what led them to build Piranha, an automated tool that eventually stripped around 2,000 stale flags out of their codebase. Most teams don’t need custom tooling for that they need a habit.
A cleanup checklist that actually gets followed
- Assign an owner to every flag the moment it’s created — no owner, no flag
- Attach an expiry date or a follow-up ticket at creation time, not after launch
- Turn on stale-flag alerts in Togglz’s console or Unleash’s dashboard so flags surface automatically
- Add a CI check that fails the build if code still references a flag marked as removed
- Remove the flag and the dead code branch in the same pull request — never split “flip it on” from “clean it up”
- Cap how many flags can be active at once; every additional flag multiplies the number of code paths you have to reason about
What We Learned
Trunk-based development and feature flags are really one practice wearing two names. Release flags let you merge unfinished work into main without breaking anyone. Ops flags give you a runtime lever when something goes wrong in production. Experiment flags let product teams test ideas without waiting on a deploy. Togglz covers the lightweight, single-service cases well; Unleash earns its extra setup cost once you need shared state, rollout percentages, or a dashboard non-engineers can use. None of that matters, though, if cleanup isn’t part of the same workflow as creation — a flag without an owner and an expiry date is just technical debt with a delay timer.




