Advanced Usage
This guide covers advanced topics for the Java SDK.
Custom Filter Evaluators
Register custom evaluators to handle custom filter types:
// Create a custom evaluator
FilterEvaluator geoEvaluator = (filter, featureKey, context) -> {
String allowedCountries = filter.getStringParameter("countries");
String userCountry = (String) context.getTrait("country");
if (allowedCountries == null || userCountry == null) {
return false;
}
return Arrays.asList(allowedCountries.split(","))
.contains(userCountry);
};
// Register it
client.getRegistry().register("GeoFilter", geoEvaluator);
Evaluator Interface
@FunctionalInterface
public interface FilterEvaluator {
boolean evaluate(FeatureFilter filter, String featureKey, EvaluationContext context);
}
Accessing Filter Parameters
FilterEvaluator myEvaluator = (filter, featureKey, context) -> {
// Get parameters
String stringValue = filter.getStringParameter("key");
double doubleValue = filter.getDoubleParameter("threshold", 0.0);
// Missing or unrecognized text is false; no boolean accessor is provided.
boolean boolValue = Boolean.parseBoolean(filter.getStringParameter("enabled"));
// Access raw parameters
Map<String, Object> params = filter.getParameters();
return true;
};
Servlet Integration (Non-Spring)
For applications using servlets without Spring:
Setup
<dependency>
<groupId>io.toggly</groupId>
<artifactId>toggly-servlet</artifactId>
<version>1.5.1</version>
</dependency>
Use a Jakarta Servlet 6.1 host such as Tomcat 11. The Java Servlet sample contains a complete runnable host, dashboard recipe and native filter examples.
Initialization and configuration
Create src/main/java/example/MyTogglyListener.java. This application override
reads TOGGLY_APP_KEY explicitly and refuses to start without it. The native
listener then creates the client and closes it when the application stops.
package example;
import io.toggly.core.config.TogglyConfig;
import io.toggly.servlet.TogglyServletContextListener;
import jakarta.servlet.ServletContext;
public class MyTogglyListener extends TogglyServletContextListener {
@Override
protected TogglyConfig createConfig(ServletContext context) {
String key = System.getenv("TOGGLY_APP_KEY");
if (key == null || key.isBlank()) {
throw new IllegalStateException("Set TOGGLY_APP_KEY before starting");
}
return TogglyConfig.builder()
.appKey(key)
.environment("Production")
.refreshIntervalSeconds(30)
.onError((message, error) -> context.log(message, error))
.build();
}
}
Register this listener once, using the web.xml entry below. Do not also
register the base listener or add @WebListener to the class. Without this
override, the native listener reads toggly.appKey or environment variable
TOGGLY_APPKEY (uppercase with dots replaced by underscores). It does not
expand a literal ${TOGGLY_APP_KEY} in web.xml.
web.xml Configuration
Place these entries inside your existing WEB-INF/web.xml descriptor. Mapping
order puts context before the native gate. Identity headers are appropriate only
behind a proxy that strips client-supplied values and supplies authenticated
identity. For application authentication and request segments, subclass
TogglyContextFilter and override createContext(ServletRequest) using the complete
request helper.
<listener>
<listener-class>example.MyTogglyListener</listener-class>
</listener>
<filter>
<filter-name>togglyContext</filter-name>
<filter-class>io.toggly.servlet.TogglyContextFilter</filter-class>
<init-param>
<param-name>identityHeader</param-name>
<param-value>X-User-Id</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>togglyContext</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>betaGate</filter-name>
<filter-class>io.toggly.servlet.FeatureGateFilter</filter-class>
<init-param>
<param-name>features</param-name>
<param-value>beta-feature</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>betaGate</filter-name>
<url-pattern>/beta/*</url-pattern>
</filter-mapping>
Deploy the WAR, create beta-feature in Production and request a route under
/beta/. The native gate returns 404 while the flag is OFF; when ON it passes
to your servlet. The strict listener above makes a missing key a startup error.
If you deliberately run without a client instead, add an application-level
unavailable guard: the native gate passes through when no client/facade is
initialized. The runnable sample uses a 503 guard and setup page for that case.
Feature flags never replace access control.
API Servlet
The native servlet has no authentication or authorization checks. Only map it behind your application's access policy; administrative refresh should not be publicly accessible. If that policy is not configured, omit this mapping.
<servlet>
<servlet-name>togglyApi</servlet-name>
<servlet-class>io.toggly.servlet.TogglyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>togglyApi</servlet-name>
<url-pattern>/api/features/*</url-pattern>
</servlet-mapping>
Endpoints:
GET /api/features- All featuresGET /api/features/{key}- Specific featurePOST /api/features/refresh- Attempt a definitions refresh
Without a client, the native API servlet returns 503. A refresh response with
status: refreshed only reports that the attempt completed; observe onError
for failed downloads. Evaluation endpoints use the current request context, so
place the context filter before this servlet too.
Static Facade
For simple use cases, use the static Toggly facade:
// Initialize once
Toggly.initialize(TogglyConfig.builder()
.appKey("your-key")
.build());
// Use anywhere
if (Toggly.isEnabled("my-feature")) {
// Feature enabled
}
// With context scope
Toggly.withContext(context, () -> {
// All evaluations use this context
processRequest();
});
// Shutdown
Toggly.shutdown();
Testing
Use real definitions with the in-memory provider to test evaluation without a
service or credentials. An ON definition has an AlwaysOn filter; a definition
with no filters is OFF. Do not pass a Map<String, Boolean> to the provider.
Place this runnable check in src/test/java/example/LocalFlags.java (or adapt
its assertions to your test framework):
package example;
import io.toggly.core.TogglyClient;
import io.toggly.core.config.TogglyConfig;
import io.toggly.core.model.FeatureDefinition;
import io.toggly.core.model.FeatureFilter;
import io.toggly.core.snapshot.InMemorySnapshotProvider;
import java.util.List;
import java.util.Map;
public class LocalFlags {
public static void main(String[] args) {
FeatureDefinition on = FeatureDefinition.builder()
.featureKey("new-feature")
.filters(List.of(FeatureFilter.alwaysOn()))
.build();
FeatureDefinition off = FeatureDefinition.builder()
.featureKey("old-feature").build();
TogglyConfig config = TogglyConfig.builder()
.appKey("local-fixture")
.enableUsageTracking(false)
.enableMetrics(false)
.registerContextsOnStartup(false)
.enableLiveUpdates(false)
.refreshIntervalSeconds(0)
.build();
InMemorySnapshotProvider provider = new InMemorySnapshotProvider(
Map.of("new-feature", on, "old-feature", off));
try (TogglyClient client = new TogglyClient(config, provider)) {
if (!client.isEnabled("new-feature") || client.isEnabled("old-feature")
|| client.isEnabled("missing-feature")) {
throw new AssertionError("Unexpected flag state");
}
System.out.println("ON, OFF and missing defaults verified");
} finally {
provider.close(); // SnapshotProvider is not AutoCloseable.
}
}
}
Run mvn test-compile dependency:copy-dependencies, then
java -cp 'target/test-classes:target/dependency/*' example.LocalFlags.
The fixture key is only a required local config value; the in-memory provider
and disabled telemetry/schema registration keep this check offline.
In a Spring test, expose that provider as a test bean and disable those same
transports in the test configuration.
Thread Safety
Share one client, but build a separate immutable EvaluationContext for each
request. Use explicit context overloads for asynchronous work. Native Servlet/MVC
context scopes are synchronous; ThreadLocal does not follow a task to another
thread. Never leave a context installed on a pooled request thread.
ContextHolder.runWithContext(context, () -> {
boolean enabled = client.isEnabled("my-feature");
}); // Restores the previous context even when application code throws.
Resource Management
A client-created HTTP provider is owned by the client:
try (TogglyClient client = new TogglyClient(config)) {
boolean enabled = client.isEnabled("my-feature");
}
When you supply a provider, close the client first and then the provider, as in
the test above. A cache wrapper owns its delegate; close only that outer provider.
Spring can own a provider through @Bean(destroyMethod = "close"). Disable
inferred destruction on a reactive wrapper around a managed core bean so there
is one owner of client shutdown. Closing releases background resources and
flushes enabled telemetry. The HTTP refresh thread is a daemon; it is not a
reason the JVM remains alive.
Logging
The SDK uses java.util.logging. Configure levels:
# logging.properties
io.toggly.level=FINE
Or programmatically:
Logger.getLogger("io.toggly").setLevel(Level.FINE);
Error Handling
Configure the native callback when building the client configuration. This
factory can also be exposed as a Spring TogglyConfig bean. Pass it the key read
from your environment; it logs failures without printing the key.
package example;
import io.toggly.core.config.TogglyConfig;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class ObservedConfiguration {
public static TogglyConfig create(String appKey) {
Logger logger = Logger.getLogger("example.features");
return TogglyConfig.builder()
.appKey(appKey)
.environment("Production")
.onError((message, error) -> logger.log(Level.WARNING, message, error))
.build();
}
}
Pass System.getenv("TOGGLY_APP_KEY") to create after checking it is present.
The HTTP provider catches network/signature failures and keeps the last good
snapshot. Before a successful first download, flags use configured defaults.
A try/catch around client.refresh() does not observe these handled failures;
normal return or refreshAsync() completion is not a success receipt. A feature
count of zero also cannot distinguish a valid empty application from a failure.
Metrics and Observability
Instrument your application service around the call if you need local timing or
counters. TogglyClient is final, so a Spring proxy aspect targeting the client
class is not a plug-and-play instrumentation mechanism. Keep evaluation and
business events separate: a flag check does not by itself mean a user used the
feature. The core evaluation path needs no external runtime libraries; optional gRPC
telemetry requires its transport dependencies and appropriate configuration.