A Java SDK for Switcher API
- Overview
- Key Features
- Installation
- Quick Start
- Configuration
- Usage Patterns
- Operating Modes
- Advanced Features
- Testing
- Native Image Support
The Switcher Client SDK is a comprehensive Java library for integrating with Switcher API, a cloud-based feature flag management system. This SDK enables you to control feature toggles, A/B testing, and configuration management in your Java applications.
- π§ Flexible Configuration: Multiple configuration approaches to fit different application architectures
- π Local & Remote Operation: Works with remote API or local snapshot files for offline capability
- π Real-time Updates: Hot-swapping support with automatic snapshot synchronization
- π§ͺ Testing Support: Built-in testing utilities and annotations for automated testing
- β‘ Performance Optimized: Includes throttling, caching, and async execution capabilities
- π‘οΈ Resilient: Silent mode with automatic fallback mechanisms for high availability
- π Easy Debugging: Comprehensive execution history and metadata tracking
Add the following dependency to your pom.xml:
<dependency>
<groupId>com.switcherapi</groupId>
<artifactId>switcher-client</artifactId>
<version>${switcher-client.version}</version>
</dependency>mvn clean install| SDK Version | Java Version | Jakarta EE | Description |
|---|---|---|---|
| v1.x | Java 8+ | No | For traditional Java EE applications |
| v2.x | Java 17+ | Yes | For Jakarta EE 9+ applications |
Here's a minimal example to get you started:
// 1. Define your feature flags
public class MyAppFeatures extends SwitcherContext {
@SwitcherKey
public static final String FEATURE_NEW_UI = "FEATURE_NEW_UI";
@SwitcherKey
public static final String FEATURE_PREMIUM = "FEATURE_PREMIUM";
}
// 2. Use in your application
public class MyService {
public void processRequest() {
if (MyAppFeatures.getSwitcher(FEATURE_NEW_UI).isItOn()) {
// Use new UI logic
} else {
// Use legacy UI logic
}
}
}This approach automatically loads configuration from a properties file, ideal for applications with externalized configuration.
Create src/main/resources/switcherapi.properties:
# Required Configuration
switcher.context=com.example.MyAppFeatures
switcher.url=https://api.switcherapi.com
switcher.apikey=YOUR_API_KEY
switcher.component=my-application
switcher.domain=MY_DOMAIN
# Optional Configuration
switcher.environment=default
switcher.timeout=3000
switcher.poolsize=2| Property | Required | Default | Description |
|---|---|---|---|
switcher.context |
β | - | Fully qualified class name extending SwitcherContext |
switcher.url |
β | - | Switcher API endpoint URL |
switcher.apikey |
β | - | API key for authentication |
switcher.component |
β | - | Your application/component identifier |
switcher.domain |
β | - | Domain name in Switcher API |
switcher.environment |
β | default | Environment name (dev, staging, default) |
switcher.local |
β | false | Enable local-only mode |
switcher.check |
β | false | Validate switcher keys on startup |
switcher.relay.restrict |
β | true | Defines if client will trigger local snapshot relay verification |
switcher.snapshot.location |
β | - | Directory for snapshot files |
switcher.snapshot.auto |
β | false | Auto-load snapshots on startup |
switcher.snapshot.skipvalidation |
β | false | Skip snapshot validation on load |
switcher.snapshot.updateinterval |
β | - | Interval for automatic snapshot updates (e.g., "5s", "2m") |
switcher.snapshot.watcher |
β | false | Monitor snapshot files for changes |
switcher.silent |
β | - | Enable silent mode (e.g., "5s", "2m") |
switcher.timeout |
β | 3000 | API timeout in milliseconds |
switcher.poolsize |
β | 2 | Thread pool size for API calls |
switcher.regextimeout (v1-only) |
β | 3000 | Time in ms given to Timed Match Worker used for local Regex (ReDoS safety mechanism) |
switcher.truststore.path |
β | - | Path to custom truststore file |
switcher.truststore.password |
β | - | Password for custom truststore |
π‘ Environment Variables: Use
${ENV_VAR:default_value}syntax for environment variable substitution.
public class MyAppFeatures extends SwitcherContext {
@SwitcherKey
public static final String FEATURE_NEW_UI = "FEATURE_NEW_UI";
@SwitcherKey
public static final String FEATURE_PREMIUM = "FEATURE_PREMIUM";
}This approach provides more flexibility and is ideal for applications requiring dynamic configuration.
public class MyAppFeatures extends SwitcherContextBase {
@SwitcherKey
public static final String FEATURE_NEW_UI = "FEATURE_NEW_UI";
// Configure programmatically
static {
configure(ContextBuilder.builder()
.context(MyAppFeatures.class.getName())
.apiKey("YOUR_API_KEY")
.url("https://api.switcherapi.com")
.domain("MY_DOMAIN")
.component("my-application")
.environment("default"));
initializeClient();
}
}@ConfigurationProperties(prefix = "switcher")
public class MySwitcherConfig extends SwitcherContextBase {
@SwitcherKey
public static final String FEATURE_NEW_UI = "FEATURE_NEW_UI";
@Override
@PostConstruct
public void configureClient() {
// Add any pre-configuration logic here
super.configureClient();
// Add any post-configuration logic here
}
}// Load from custom properties file
MyAppFeatures.loadProperties("switcherapi-test");Feature flags must follow specific conventions for proper functionality:
public class MyAppFeatures extends SwitcherContext {
// β
Correct: public static final String
@SwitcherKey
public static final String FEATURE_NEW_UI = "FEATURE_NEW_UI";
@SwitcherKey
public static final String FEATURE_PREMIUM_ACCESS = "FEATURE_PREMIUM_ACCESS";
// β Incorrect examples:
// private static final String WRONG = "WRONG"; // Not public
// public static String WRONG2 = "WRONG2"; // Not final
// public final String WRONG3 = "WRONG3"; // Not static
}Why these conventions matter:
public: Accessible from other parts of your applicationstatic: No need to instantiate the class to access the constantfinal: Prevents accidental modification during runtime
You can also name your feature flag attributes differently, but ensure the values match those defined in Switcher API.
// Simple boolean check
Switcher switcher = MyAppFeatures.getSwitcher(FEATURE_NEW_UI);
if (switcher.isItOn()) {
// Feature is enabled
}Switcher switcher = MyAppFeatures.getSwitcher(FEATURE_NEW_UI);
SwitcherResult result = switcher.submit();
if (result.isItOn()) {
System.out.println("Feature enabled: " + result.getReason());
// Access additional metadata if needed
MyMetadata metadata = result.getMetadata(MyMetadata.class);
}List<Entry> entries = new ArrayList<>();
entries.add(Entry.of(StrategyValidator.DATE, "2024-01-01"));
entries.add(Entry.of(StrategyValidator.TIME, "14:00"));
switcher.prepareEntry(entries);
boolean isEnabled = switcher.isItOn();import static com.example.MyAppFeatures.*;
boolean isEnabled = getSwitcher(FEATURE_PREMIUM_ACCESS)
.checkValue("premium_user")
.checkNetwork("192.168.1.0/24")
.checkDate("2024-01-01")
.isItOn();Switcher switcher = getSwitcher(FEATURE_NEW_UI)
.keepExecutions() // Enable execution tracking
.checkValue("user_type");
switcher.isItOn();
// Access the last execution result
SwitcherResult lastResult = switcher.getLastExecutionResult();
System.out.println("Last execution reason: " + lastResult.getReason());
// Clear history when needed
switcher.flushExecutions();// Execute asynchronously with 1-second throttle
// Returns cached result if called within throttle period
boolean isEnabled = switcher.throttle(1000).isItOn();Default mode that communicates directly with Switcher API.
MyAppFeatures.configure(ContextBuilder.builder()
.url("https://api.switcherapi.com")
.apiKey("YOUR_API_KEY")
.domain("MY_DOMAIN")
.component("my-app"));
MyAppFeatures.initializeClient();Use Cases:
- Real-time feature flag updates
- A/B testing with immediate changes
- Centralized configuration management
Uses local snapshot files without API communication.
MyAppFeatures.configure(ContextBuilder.builder()
.local(true)
.snapshotLocation("./src/main/resources/snapshots"));
MyAppFeatures.initializeClient();Use Cases:
- Offline environments
- High-performance scenarios where API latency is critical
- Development and testing environments
Combines remote and local capabilities for optimal flexibility.
// Force specific switcher to resolve remotely even in local mode
switcher.forceRemote().isItOn();MyAppFeatures.configure(ContextBuilder.builder()
.url("https://api.switcherapi.com")
.apiKey("YOUR_API_KEY")
.domain("MY_DOMAIN")
.local(true)
.snapshotAutoLoad(true)
.snapshotAutoUpdateInterval("30s") // Check for updates every 30 seconds
.component("my-app"));
MyAppFeatures.initializeClient();
// Optional: Schedule with callback for monitoring
MyAppFeatures.scheduleSnapshotAutoUpdate("30s", new SnapshotCallback() {
@Override
public void onSnapshotUpdate(long version) {
logger.info("Snapshot updated to version: {}", version);
}
@Override
public void onSnapshotUpdateError(Exception e) {
logger.error("Failed to update snapshot: {}", e.getMessage());
}
});This feature allows you to specify how long the client SDK should attempt to restore connectivity in case of remote API failures.
When the API is unavailable, the SDK will automatically operate in silent mode, evaluating Switchers using a local snapshot. It is important to note that any Switcher Key configured must be able to resolve without external dependencies (e.g., Switcher Relay).
Make sure to configure the scheduled snapshot auto-update to keep the local snapshot up to date with the remote API.
Here is an example - in-memory snapshot with auto-update every 30 seconds:
MyAppFeatures.configure(ContextBuilder.builder()
.context(MyAppFeatures.class.getName())
.apiKey("YOUR_API_KEY")
.url("https://api.switcherapi.com")
.domain("MY_DOMAIN")
.component("my-application")
.silentMode("5m")
.snapshotAutoUpdateInterval("30s")
);Monitor snapshot files for external changes:
// Start watching for file changes
MyAppFeatures.watchSnapshot();
// Stop watching when no longer needed
MyAppFeatures.stopWatchingSnapshot();Or enable during initialization:
MyAppFeatures.configure(ContextBuilder.builder()
.snapshotWatcher(true)
.snapshotLocation("./src/main/resources/snapshots"));// Check if remote snapshot is newer than local
boolean hasUpdates = MyAppFeatures.validateSnapshot();
if (hasUpdates) {
logger.info("New snapshot version available");
}MyAppFeatures.configure(ContextBuilder.builder()
.snapshotAutoUpdateInterval("5m") // Check every 5 minutes
.snapshotLocation("./src/main/resources/snapshots"));MyAppFeatures.configure(ContextBuilder.builder()
.timeoutMs(5000) // 5 second timeout
.poolConnectionSize(5) // 5 concurrent connections
// ... other config
);@Test
void testFeatureEnabled() {
// Force switcher to return specific value
SwitcherBypass.assume(FEATURE_NEW_UI, true);
assertTrue(myService.usesNewUI());
// Reset to original behavior
SwitcherBypass.forget(FEATURE_NEW_UI);
}
@Test
void testWithConditions() {
Switcher switcher = MyAppFeatures.getSwitcher(FEATURE_PREMIUM_ACCESS)
.checkValue("user_type");
// Assume true only when specific condition is met
SwitcherBypass.assume(FEATURE_PREMIUM_ACCESS, true)
.when(StrategyValidator.VALUE, "premium");
assertTrue(switcher.isItOn());
}@SwitcherTest(key = FEATURE_NEW_UI, result = true)
void testNewUIFeature() {
// FEATURE_NEW_UI will return true during this test
assertTrue(myService.usesNewUI());
// Automatically resets after test completion
}@SwitcherTest(switchers = {
@SwitcherTestValue(key = FEATURE_NEW_UI, result = true),
@SwitcherTestValue(key = FEATURE_PREMIUM_ACCESS, result = false)
})
void testMultipleFeatures() {
assertTrue(myService.usesNewUI());
assertFalse(myService.hasPremiumAccess());
}@SwitcherTest(key = FEATURE_NEW_UI, abTest = true)
void testFeatureABTesting() {
// Test passes regardless of switcher result
// Useful for testing both code paths
myService.handleUILogic();
}@SwitcherTest(
key = FEATURE_PREMIUM_ACCESS,
result = true,
when = @SwitcherTestWhen(value = "premium_user")
)
void testPremiumFeature() {
// Test with specific input conditions
assertTrue(myService.checkPremiumAccess("premium_user"));
}Validate all switcher keys are properly configured:
@Test
void validateSwitcherConfiguration() {
// Throws exception if any switcher key is not found
assertDoesNotThrow(() -> MyAppFeatures.checkSwitchers());
}Enable automatic validation during startup:
MyAppFeatures.configure(ContextBuilder.builder()
.checkSwitchers(true) // Validate on initialization
// ... other config
);Switcher Client fully supports GraalVM Native Image compilation:
@ConfigurationProperties
public class MyNativeAppFeatures extends SwitcherContextBase {
public static final String FEATURE_NEW_UI = "FEATURE_NEW_UI";
public static final String FEATURE_PREMIUM = "FEATURE_PREMIUM";
@Override
@PostConstruct
protected void configureClient() {
super.registerSwitcherKeys(FEATURE_NEW_UI, FEATURE_PREMIUM);
super.configureClient();
}
}- π Switcher Tutorials - Complete code examples and tutorials
- π¬ Join our Slack - Community support and discussions
- π Report Issues - Bug reports and feature requests
This project's core foundation was built and maintained by a human from day one. However, we have leveraged AI tools to assist in various aspects of the development process, such as troubleshooting, code optimization, and documentation. We have thoroughly reviewed and tested all AI-generated contributions to ensure they meet our quality standards and align with our project's goals. We are committed to transparency about our use of AI and will continue to disclose any significant AI contributions in the future.
External contributions from the community are equally valued and will be reviewed with the same standards, regardless of whether they were assisted by AI or not. We encourage all contributors to disclose their use of AI tools in their contributions to maintain transparency and foster trust within our community.
