Skip to main content

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.

MyTogglyListener.java
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 features
  • GET /api/features/{key} - Specific feature
  • POST /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):

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

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