Skip to main content

Java

Install the latest version

Github | Maven Repository

<dependency>
<groupId>com.reforge</groupId>
<artifactId>sdk</artifactId>
<version>LATEST</version>
</dependency>

Dependency-Reduced Version

There's an optional uber-jar with shaded and relocated guava and failsafe dependencies

<dependency>
<groupId>com.reforge</groupId>
<artifactId>client</artifactId>
<version>0.3.20</version>
<classifier>uberjar</classifier>
</dependency>

Initialize the client

Basic Usage

import com.reforge.sdk.Options;
import com.reforge.sdk.Sdk;

final Sdk reforge = new Sdk(new Options());

Typical Usage

We recommend using the Sdk as a singleton in your application. This is the most common way to use the SDK.

import com.reforge.sdk.Options;
import com.reforge.sdk.Sdk;

// Micronaut Factory
@Factory
public class ReforgeFactory {

@Singleton
public Sdk reforge() {
return new Sdk(new Options());
}

@Singleton
public FeatureFlagClient featureFlagClient(Sdk reforge) {
return reforge.featureFlagClient();
}

@Singleton
public ConfigClient configClient(Sdk reforge) {
return reforge.configClient();
}
}

Feature Flags

For boolean flags, you can use the featureIsOn convenience method:

public class MyClass {
// assumes you have setup a singleton
@Inject
private FeatureFlagClient featureFlagClient;

public String test(String key){
boolean val = featureFlagClient.featureIsOn(key);
return "Feature flag value of %s is %s".formatted(key, val);
}
}

Feature flags don't have to return just true or false. You can get other data types using get:

public class MyClass {
// assumes you have setup a singleton
@Inject
private FeatureFlagClient featureFlagClient;

public String test(String key){
Optional<Prefab.ConfigValue> val = featureFlagClient.get(key);
return "Feature flag value of %s is %s".formatted(key, val.orElse("no value found"));
}
}

Context

To finely-target configuration rule evaluation, we accept contextual information globally, request-scoped (thread-locally) with the ContextStore which will affect all featureflag and config lookups.

Global Context

Use global context for information that doesn't change - for example, your application's key, availability-zone etc. Set it in the client's options as below

import com.reforge.sdk.Options;
import com.reforge.sdk.Sdk;

Context deploymentContext = Context
.newBuilder("application")
.put("key", "my-api")
.put("az", "1a")
.put("type", "web")
.build();

Options options = new Options()
.setGlobalContext(ContextSet.from(deploymentContext));

final Sdk reforge = new Sdk(options);

Thread-local (Request-scoped)

// set the thread-local context
reforge.configClient().getContextStore().addContext(
Context.newBuilder("User")
.put("name", user.getName())
.put("key", user.getKey())
.build());

// or using an autoclosable scope helper
// this will replace any-existing threadlocal context until the try-with-resources block exits
ContextHelper prefabContextHelper = new ContextHelper(
reforge.configClient()
);

try (
ContextHelper.ContextScope ignored = prefabContextHelper.performWorkWithAutoClosingContext(
Context.newBuilder("User")
.put("name", user.getName())
.put("key", user.getKey())
.build());
) {
// do config/flag operations
}

When thread-local context is set, log levels and feature flags will evaluate in that context. Here are details on setting thread-local context:

Add a filter to add a Reforge context based on the currently "logged in" user.

@Filter(Filter.MATCH_ALL_PATTERN)
public class ContextFilter implements HttpFilter {

private final ConfigClient configClient;

@Inject
ContextFilter(ConfigClient configClient) {
this.configClient = configClient;
}

@Override
public Publisher<? extends HttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {

request.getUserPrincipal(Authentication.class).ifPresent(authentication ->
{
User user = (User) authentication.getAttributes().get(ExampleAuthenticationProvider.USER_ATTR);
configClient.getContextStore()
.addContext(
Context.newBuilder("user")
.put("id", user.id())
.put("country", user.country())
.put("email", user.email())
.build()
);
}
);
return chain.proceed(request);
}

@Override
public int getOrder() {
return ServerFilterPhase.SECURITY.after() + 1;
// run after the DefaultLoginFilter
}
}

Reforge Context uses ThreadLocals by default. In event-based frameworks like micronaut, that won't work so configure the Reforge Context store to use ServerRequestContextStore instead.

options.setContextStore(new ServerRequestContextStore());

Learn more with the Reforge + Micronaut example app

Just-in-time Context

You can also provide context information inline when making a get request. If you provide just-in-time context to your FF or config evaluations, it will be merged with the global context.

featureFlagClient.featureIsOn(
"features.example-flag",
Context.newBuilder("customer")
.put("group", "beta")
.build()
)

reforge.configClient().get("the.key",
Context.newBuilder("user")
.put("name", "james")
.put("tier", "gold")
.put("customerMonths", 12)
.build()
)

See contexts for more information

Dynamic Config

final Optional<Prefab.ConfigValue> configValue = reforge.configClient().get("the.key");
if(configValue.isPresent()){
System.out.println(configValue.get().getString());
}

Live Values

Live values are a convenient and clear way to use configuration throughout your system. Inject a Reforge client and get live values for the configuration keys you need.

In code, .get() will return the value. These values will update automatically when the configuration is updated in Reforge Launch.

import java.util.function.Supplier;

class MyClass {

private Supplier<String> sampleString;
private Supplier<Long> sampleLong;

@Inject
public MyClass(ConfigClient configClient) {
this.sampleString = configClient.liveString("sample.string");
this.sampleLong = configClient.liveLong("sample.long");
}

public String test(){
return "I got %s and %d from Reforge Launch.".formatted(sampleString.get(), sampleLong.get());
}
}

Telemetry

By default, Reforge uploads telemetry that enables a number of useful features. You can alter or disable this behavior using the following options:

NameDescriptionDefault
collectEvaluationSummariesSend counts of config/flag evaluation results back to Reforge to view in web apptrue
collectLoggerCountsSend counts of logger usage back to Reforge to power log-levels configuration screentrue
contextUploadModeUpload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contextsPERIODIC_EXAMPLE

If you want to change any of these options, you can pass an options object when initializing the Reforge client.

Options options = new Options()
.setCollectEvaluationSummaries(true)
.setCollectLoggerCounts(true)
.setContextUploadMode(Options.CollectContextMode.PERIODIC_EXAMPLE);

Testing

Reforge suggests testing with generous usage of Mockito. We also provide a useful FixedValue for testing Live Values.

@Test
void testReforge(){
ConfigClient mockConfigClient = mock(ConfigClient.class);
when(mockConfigClient.liveString("sample.string")).thenReturn(FixedValue.of("test value"));
when(mockConfigClient.liveLong("sample.long")).thenReturn(FixedValue.of(123L));

MyClass myClass = new MyClass(mockConfigClient);

// test business logic

}

Reference

Options

Options options = new Options()
.setConfigOverrideDir(System.getProperty("user.home"))
.setSdkKey(System.getenv("REFORGE_BACKEND_SDK_KEY"))
.setPrefabDatasource(Options.Datasources.ALL) // Option: Datasources.LOCAL_ONLY
.setOnInitializationFailure(Options.OnInitializationFailure.RAISE) // Option Options.OnInitializationFailure.UNLOCK
.setInitializationTimeoutSec(10)
.setGlobalContext(ContextSet.from(Context
.newBuilder("application")
.put("key", "my-api")
.put("az", "1a")
.put("type", "web")
.build())
);

Option Definitions

NameDescriptionDefault
collectEvaluationSummariesSend counts of config/flag evaluation results back to Reforge to view in web apptrue
collectLoggerCountsSend counts of logger usage back to Reforge to power log-levels configuration screentrue
contextUploadModeUpload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contextsPERIODIC_EXAMPLE
onInitializationFailureChoose to crash or continue with local data only if unable to fetch config data from Reforge at startupRAISE (crash)
prefabDatasourcesUse either only-local data or local + API dataALL
globalContextset a static context to be used as the base layer in all configuration evaluationEMPTY