> If you are setting up, configuring, or answering questions about Schematic, connect the
> Schematic MCP server before working from these pages. It reads and writes plans, features,
> entitlements, and overrides against the real account, so what you report and what you build
> match what actually exists.
>
> The server is at https://api.schematichq.com/mcp
>
> From Claude, ChatGPT, or another chat assistant, add it as a custom connector:
> https://docs.schematichq.com/working-with-ai
>
> From a coding assistant such as Claude Code, Codex, or Cursor:
> https://docs.schematichq.com/building-with-ai
>
> For clean Markdown of any page, append `.md` to the page URL. For a complete page index,
> see https://docs.schematichq.com/llms.txt
# Java
> Use schematic-java to easily implement pricing and packaging in your java app with Schematic.
The official Schematic Java library.
**Java version requirements:**
* Core SDK (flag checks, events, webhooks): **Java 8+**
* Datastream / local flag evaluation: **Java 11+** (required by the WASM runtime)
Enabling datastream on a JVM older than 11 will throw a `DataStreamException` with a clear message at `Schematic` build time, rather than a cryptic `UnsupportedClassVersionError` at runtime.
## Installation and Setup
1. Add the dependency using your build tool of choice:
Using Gradle in `build.gradle`:
```groovy
dependencies {
implementation 'com.schematichq:schematic-java:0.x.x'
}
```
Using Maven in `pom.xml`:
```xml
com.schematichq
schematic-java
0.x.x
```
2. [Issue an API key](/quickstart/account-setup#2-create-your-api-keys) for the appropriate environment using the [Schematic app](https://app.schematichq.com/settings/api-keys).
3. Using this secret key, initialize a client in your application:
```java
import com.schematic.api.Schematic;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.build();
```
## Usage
A number of these examples use `keys` to identify companies and users. Learn more about keys [here](/developer_resources/key_management).
### Sending identify events
Create or update users and companies using identify events.
```java
import com.fasterxml.jackson.databind.JsonNode;
import com.schematic.api.Schematic;
import com.schematic.api.core.ObjectMappers;
import java.util.HashMap;
import java.util.Map;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.build();
Map keys = new HashMap<>();
keys.put("email", "wcoyote@acme.net");
keys.put("user_id", "your-user-id");
EventBodyIdentifyCompany company = EventBodyIdentifyCompany.builder()
.name("Acme Widgets, Inc.")
.build();
Map traits = new HashMap<>();
traits.put("city", ObjectMappers.JSON_MAPPER.valueToTree("Atlanta"));
traits.put("high_score", ObjectMappers.JSON_MAPPER.valueToTree(25));
traits.put("is_active", ObjectMappers.JSON_MAPPER.valueToTree(true));
schematic.identify(keys, company, "Wile E. Coyote", traits);
```
This call is non-blocking and there is no response to check.
### Sending track events
Track activity in your application using track events; these events can later be used to produce metrics for targeting.
```java
import com.schematic.api.Schematic;
import java.util.HashMap;
import java.util.Map;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.build();
Map company = new HashMap<>();
company.put("id", "your-company-id");
Map user = new HashMap<>();
user.put("user_id", "your-user-id");
Map traits = new HashMap<>();
schematic.track("some-action", company, user, traits);
```
This call is non-blocking and there is no response to check.
If you want to record large numbers of the same event at once, or perhaps measure usage in terms of a unit like tokens or memory, you can optionally specify a quantity for your event:
```java
schematic.track("some-action", company, user, traits, 10);
```
### Creating and updating companies
Although it is faster to create companies and users via identify events, if you need to handle a response, you can use the companies API to upsert companies. Because you use your own identifiers to identify companies, rather than a Schematic company ID, creating and updating companies are both done via the same upsert operation:
```java
import com.fasterxml.jackson.databind.JsonNode;
import com.schematic.api.Schematic;
import com.schematic.api.core.ObjectMappers;
import com.schematic.api.types.UpsertCompanyRequestBody;
import java.util.HashMap;
import java.util.Map;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.build();
Map keys = new HashMap<>();
keys.put("id", "your-company-id");
Map traits = new HashMap<>();
traits.put("city", ObjectMappers.JSON_MAPPER.valueToTree("Atlanta"));
traits.put("high_score", ObjectMappers.JSON_MAPPER.valueToTree(25));
traits.put("is_active", ObjectMappers.JSON_MAPPER.valueToTree(true));
UpsertCompanyRequestBody request = UpsertCompanyRequestBody.builder()
.keys(keys)
.name("Acme Widgets, Inc.")
.traits(traits)
.build();
var response = schematic.companies().upsertCompany(request);
System.out.println("Company upserted: " + response.getData().getName());
```
### Checking flags
When checking a flag, you'll provide keys for a company and/or keys for a user. You can also provide no keys at all, in which case you'll get the default value for the flag.
```java
import com.schematic.api.Schematic;
import java.util.HashMap;
import java.util.Map;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.build();
Map company = new HashMap<>();
company.put("id", "your-company-id");
Map user = new HashMap<>();
user.put("user_id", "your-user-id");
boolean flagValue = schematic.checkFlag("some-flag-key", company, user);
```
## Webhook Verification
Schematic can send webhooks to notify your application of events. To ensure the security of these webhooks, Schematic signs each request using HMAC-SHA256. The Java SDK provides utility functions to verify these signatures.
### Verifying Webhook Signatures
When your application receives a webhook request from Schematic, you should verify its signature to ensure it's authentic:
```java
import com.schematic.webhook.WebhookVerifier;
import com.schematic.webhook.WebhookSignatureException;
import java.util.Map;
import java.util.HashMap;
import java.io.BufferedReader;
import java.io.IOException;
// In your webhook endpoint handler:
public void handleWebhook(HttpServletRequest request, HttpServletResponse response) throws IOException {
// Read the request body
String body = request.getReader().lines().collect(Collectors.joining("\n"));
// Get the required headers
Map headers = new HashMap<>();
headers.put(WebhookVerifier.WEBHOOK_SIGNATURE_HEADER,
request.getHeader(WebhookVerifier.WEBHOOK_SIGNATURE_HEADER));
headers.put(WebhookVerifier.WEBHOOK_TIMESTAMP_HEADER,
request.getHeader(WebhookVerifier.WEBHOOK_TIMESTAMP_HEADER));
String webhookSecret = "your-webhook-secret";
try {
// Verify the webhook signature
WebhookVerifier.verifyWebhookSignature(body, headers, webhookSecret);
// Process the webhook payload
// ...
response.setStatus(HttpServletResponse.SC_OK);
} catch (WebhookSignatureException e) {
// Handle signature verification failure
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid signature: " + e.getMessage());
}
}
```
### Verifying Signatures Manually
If you need to verify a webhook signature outside of the context of a servlet request, you can use the `verifySignature` method:
```java
import com.schematic.webhook.WebhookVerifier;
import com.schematic.webhook.WebhookSignatureException;
public void verifyWebhookManually(String body, String signature, String timestamp, String secret) {
try {
WebhookVerifier.verifySignature(body, signature, timestamp, secret);
System.out.println("Signature verification successful!");
} catch (WebhookSignatureException e) {
System.out.println("Signature verification failed: " + e.getMessage());
}
}
```
## Configuration Options
There are a number of configuration options that can be specified using the builder when instantiating the Schematic client.
### Flag Check Options
By default, the client will do some local caching for flag checks. If you would like to change this behavior, you can do so using initialization options to specify the cache providers:
```java
import com.schematic.api.Schematic;
import com.schematic.api.cache.LocalCache;
import java.time.Duration;
import java.util.Collections;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.cacheProviders(Collections.singletonList(new LocalCache<>()))
.build();
```
You can also disable local caching entirely; bear in mind that, in this case, every flag check will result in a network request:
```java
import com.schematic.api.Schematic;
import java.util.Collections;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.cacheProviders(Collections.emptyList())
.build();
```
You may want to specify default flag values for your application, which will be used if there is a service interruption or if the client is running in offline mode:
```java
import com.schematic.api.Schematic;
import java.util.HashMap;
import java.util.Map;
Map flagDefaults = new HashMap<>();
flagDefaults.put("some-flag-key", true);
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.flagDefaults(flagDefaults)
.build();
```
### Offline Mode
In development or testing environments, you may want to avoid making network requests when checking flags or submitting events. You can run Schematic in offline mode:
```java
import com.schematic.api.Schematic;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.offline(true)
.build();
```
When in offline mode:
1. Flag checks will return the default value for the flag being checked (false by default, or as specified in flagDefaults)
2. Events (identify and track) will be skipped completely
3. All other API calls will use a no-op HTTP client that doesn't make actual network requests, returning empty responses
This is especially useful for development, testing, or when running unit tests that shouldn't depend on the Schematic API.
Offline mode works well with flag defaults:
```java
import com.schematic.api.Schematic;
import java.util.HashMap;
import java.util.Map;
Map flagDefaults = new HashMap<>();
flagDefaults.put("some-flag-key", true);
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.offline(true)
.flagDefaults(flagDefaults)
.build();
boolean flagValue = schematic.checkFlag("some-flag-key", null, null); // Returns true
```
### Event Buffer
Schematic API uses an Event Buffer to batch Identify and Track requests and avoid multiple API calls. You can set the event buffer flush period:
```java
import com.schematic.api.Schematic;
import java.time.Duration;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.eventBufferInterval(Duration.ofSeconds(5))
.build();
```
## Exception Handling
When the API returns a non-success status code (4xx or 5xx response), a subclass of `SchematicException`
will be thrown.
```java
import com.schematic.api.core.SchematicException;
try {
schematic.companies().getCompany(...);
} catch (SchematicException e) {
System.out.println(e.message());
}
```
The SDK also supports error handling for first class exceptions with strongly typed body fields.
```java
import com.schematic.api.errors.InvalidRequestError;
try {
schematic.companies().getCompany(...);
} catch (InvalidRequestError e) {
System.out.println(e.message());
System.out.println(e.getBody().getMissingField());
}
```
## Retries
The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long
as the request is deemed retriable and the number of retry attempts has not grown larger than the configured
retry limit (default: 2).
A request is deemed retriable when any of the following HTTP status codes is returned:
* [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
* [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
* [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)
## DataStream
DataStream enables local flag evaluation by maintaining a WebSocket connection to Schematic and caching flag rules, company, and user data locally. This reduces latency and network calls for flag checks.
### Key Features
* **Real-Time Updates**: Automatically updates cached data when changes occur on the backend.
* **Configurable Caching**: Supports both in-memory (local) caching and Redis-based caching.
* **Efficient Flag Checks**: Flag evaluation happens locally using a WASM rules engine.
### Setup
```java
import com.schematic.api.Schematic;
import com.schematic.api.datastream.DatastreamOptions;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.datastreamOptions(DatastreamOptions.builder()
.build())
.build();
// Flag checks are now evaluated locally
boolean flagValue = schematic.checkFlag("some-flag-key", company, user);
// When done, close the client to release resources
schematic.close();
```
### Configuration Options
| Option | Type | Default | Description |
| ------------ | ------------------ | -------- | -------------------------------------------------------------- |
| `cacheTTL` | `Duration` | 24 hours | Cache TTL for flag/company/user data |
| `redisCache` | `RedisCacheConfig` | — | Redis connection config (uses in-memory cache if not provided) |
### Configuring Redis Cache
DataStream supports Redis for caching, which is required for [Replicator Mode](#replicator-mode). Pass a `RedisCacheConfig` and the SDK will create and manage the Redis connection internally:
```java
import com.schematic.api.Schematic;
import com.schematic.api.cache.RedisCacheConfig;
import com.schematic.api.datastream.DatastreamOptions;
import java.time.Duration;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.datastreamOptions(DatastreamOptions.builder()
.redisCache(RedisCacheConfig.builder()
.endpoint("localhost:6379")
.keyPrefix("schematic:")
.build())
.cacheTTL(Duration.ofMinutes(5))
.build())
.build();
```
#### Redis Configuration Options
| Option | Type | Default | Description |
| ---------------- | -------------- | -------------------- | ----------------------------------------------- |
| `endpoint` | `String` | `localhost:6379` | Redis server address in `host:port` format |
| `endpoints` | `List` | `["localhost:6379"]` | Multiple endpoints (for future cluster support) |
| `username` | `String` | — | Redis 6.0+ ACL username |
| `password` | `String` | — | Redis password |
| `database` | `int` | `0` | Redis database index |
| `ssl` | `boolean` | `false` | Enable SSL/TLS |
| `keyPrefix` | `String` | `schematic:` | Prefix for all Redis cache keys |
| `connectTimeout` | `Duration` | 5 seconds | Connection timeout |
| `readTimeout` | `Duration` | 3 seconds | Read timeout |
| `maxPoolSize` | `int` | 8 | Maximum connection pool size |
## Replicator Mode
Replicator mode is designed for environments where a separate process (the [schematic-datastream-replicator](https://github.com/SchematicHQ/schematic-datastream-replicator)) manages the WebSocket connection and populates a shared Redis cache. The SDK reads from that cache and evaluates flags locally without establishing its own WebSocket connection.
### Requirements
Replicator mode **requires Redis** as a shared cache so the SDK can read data written by the external replicator process. An in-memory cache will not work since the replicator and SDK run in separate processes.
### Setup
```java
import com.schematic.api.Schematic;
import com.schematic.api.cache.RedisCacheConfig;
import com.schematic.api.datastream.DatastreamOptions;
Schematic schematic = Schematic.builder()
.apiKey("YOUR_API_KEY")
.datastreamOptions(DatastreamOptions.builder()
.redisCache(RedisCacheConfig.builder()
.endpoint("localhost:6379")
.build())
.withReplicatorMode("http://localhost:8090/ready")
.build())
.build();
```
### Configuration Options
| Option | Type | Default | Description |
| ------------------------------- | ------------------ | ---------- | ---------------------------------------------------------- |
| `withReplicatorMode` | `String` | — | Enables replicator mode with the given health check URL |
| `redisCache` | `RedisCacheConfig` | — | **Required.** Redis connection config for the shared cache |
| `replicatorHealthCheckInterval` | `Duration` | 30 seconds | Health check polling interval |
| `cacheTTL` | `Duration` | 24 hours | Cache TTL (should match the replicator's TTL) |
### Cache TTL Configuration
**Important:** When using Replicator Mode, you should set the SDK's cache TTL to match the replicator's cache TTL. The replicator defaults to an unlimited cache TTL. If the SDK uses a shorter TTL (the default is 24 hours), locally updated cache entries (e.g. after track events) will be written back with the shorter TTL and eventually evicted from the shared Redis cache, even though the replicator originally set them with no expiration.
To match the replicator's default unlimited TTL:
```java
DatastreamOptions.builder()
.redisCache(RedisCacheConfig.builder()
.endpoint("localhost:6379")
.build())
.withReplicatorMode("http://localhost:8090/ready")
.cacheTTL(Duration.ZERO) // Unlimited, matching the replicator default
.build()
```
When running in Replicator Mode, the client will:
* Skip establishing WebSocket connections
* Periodically check if the replicator service is ready
* Use cached data populated by the external replicator service
* Fall back to direct API calls if the replicator is not available
> Use schematic-java to easily implement pricing and packaging in your java app with Schematic.