stellnula-java-sdk is the pure Java client SDK for the StellNula configuration center. It is designed for plain Java applications, platform components, and a future Spring Boot Starter. The SDK provides startup synchronization, remote configuration reads, local in-memory snapshots, local directory snapshots, gRPC Watch, failure recovery, type conversion, prefix binding, fine-grained listeners, and OpenTelemetry metrics.
This repository contains only the core SDK. It does not depend on Spring Boot, Spring Framework, or any auto-configuration mechanism. Spring Boot integration, runtime framework wiring, and property-source adaptation should live in a separate starter module built on top of this SDK.
| Item | Description |
|---|---|
| Stability | In development |
| Java version | JDK 25 |
| Project type | Configuration center Java SDK |
| Maven coordinates | io.github.stellhub:stellnula-java-sdk |
| Core transports | HTTP data plane, gRPC Watch |
| Target users | Java applications, framework starters, platform middleware |
| Maintainer | StellHub |
- Fetches a full remote configuration snapshot from
stellnula-serviceduring application startup. - Stores configuration in an in-memory snapshot and persists each config item into a local snapshot directory.
- Uses revision and checksum to detect stale or inconsistent data.
- Watches runtime changes through gRPC Watch and falls back to HTTP recovery when needed.
- Exposes key reads, type conversion, prefix binding, change listeners, and management APIs.
- Accepts externally managed OkHttpClient, ExecutorService, and OpenTelemetry instances so frameworks can manage connection pools, thread pools, and telemetry export consistently.
- It does not implement
stellnula-service. - It does not provide Spring Boot auto-configuration,
EnvironmentPostProcessor, or property injection. - It does not implement a configuration management console.
- It does not treat local snapshots as the source of truth. Local snapshots are a startup fallback and diagnostic artifact.
- It does not create OpenTelemetry SDKs, MeterProviders, or exporters internally.
| Feature | Description |
|---|---|
| Bootstrap | Fetches a full snapshot during startup |
| Config Read | Reads config values by configKey or configId |
| Local Memory Snapshot | Keeps the current configuration view as an immutable snapshot |
| Local Directory Snapshot | Persists each config item under a local configs/ directory |
| Revision and Checksum | Validates consistency with server revision and checksum |
| gRPC Watch | Long-polls runtime configuration changes |
| Recovery | Falls back to HTTP delta/full sync when Watch fails |
| Listener | Supports all-change, key-based, and prefix-based listeners |
| Type Conversion | Converts values to String, numbers, Boolean, Duration, and more |
| Prefix Binding | Binds config values under a prefix to a Java type |
| Telemetry | Records metrics through an externally provided OpenTelemetry instance |
| Management API | Provides basic config, governance-rule, and gray-rule management calls |
The SDK runtime model has four layers:
-
Client context
StellnulaClientOptionsdescribes the identity and runtime scope of a client instance, includingappId,clientId,env,region,zone,cluster,namespace,group, subscriptions, snapshot directory, timeouts, retry settings, executors, and telemetry. -
Remote synchronization
On startup, the SDK loads the local snapshot first, then performs an HTTP bootstrap against the server. The server returns config entries, revision, checksum, and optional gRPC endpoints. The SDK verifies consistency and writes the latest state to memory and disk.
-
Local snapshot
The current configuration view is represented by
StellnulaSnapshot. Each config item is aStellnulaConfigEntrycontainingconfigId,configKey,contentType,configValue, version, gray-match metadata, delivery mode, and scope. Public reads are usuallyconfigKey -> configValue, while internal delta merging uses the more stableconfigId. -
Runtime changes
The SDK uses gRPC Watch to observe server-side revision changes. When changes arrive, it applies the delta, updates local snapshots, notifies listeners, and records metrics. If a gRPC node fails, the SDK isolates the failed node and recovers through HTTP delta/full sync.
flowchart LR
App["Java Application"] --> Client["StellnulaClient"]
Client --> Options["StellnulaClientOptions"]
Client --> Cache["Memory Snapshot"]
Client --> Store["Directory Snapshot"]
Client --> Http["HTTP Transport"]
Client --> Grpc["gRPC Watch Client"]
Client --> Telemetry["OpenTelemetry Metrics"]
Http --> Service["stellnula-service"]
Grpc --> Service
sequenceDiagram
participant App as Java App
participant SDK as StellnulaClient
participant Store as Local Snapshot
participant Service as stellnula-service
App->>SDK: start()
SDK->>Store: load()
Store-->>SDK: optional local snapshot
SDK->>Service: HTTP bootstrap(currentRevision)
Service-->>SDK: configs + revision + checksum + gRPC endpoints
SDK->>SDK: verify checksum and replace memory snapshot
SDK->>Store: save directory snapshot
SDK->>Service: heartbeat()
SDK->>Service: start gRPC watch loop
sequenceDiagram
participant Service as stellnula-service
participant Watch as gRPC Watch
participant SDK as StellnulaClient
participant Listener as Config Listener
SDK->>Watch: watch(current revision/checksum)
Watch->>Service: long polling request
Service-->>Watch: config changes or metadata change
Watch-->>SDK: WatchResult
SDK->>SDK: apply changes by configId
SDK->>SDK: persist snapshot and record metrics
SDK-->>Listener: filtered change event
| Concept | Description |
|---|---|
appId |
Application identifier used to locate the application's config set |
clientId |
Client instance identifier, preferably unique per process |
env |
Environment such as dev, uat, pre, or prod |
namespace |
Configuration isolation scope, often used for tenant, domain, or environment isolation |
group |
Configuration group. Currently used as the default client group; subscriptions can specify their own group |
configId |
Server-side unique config identifier, preferred for internal delta merging |
configKey |
Business-facing read key; it can also represent a file path such as application/dev.yaml |
configValue |
Decoded configuration content. For file configs, this is the file content |
revision |
Server-side configuration version used to decide whether synchronization is needed |
checksum |
Config-set checksum used to detect local/server inconsistency |
subscription |
Client-side subscription filter for all configs or selected configs |
<dependency>
<groupId>io.github.stellhub</groupId>
<artifactId>stellnula-java-sdk</artifactId>
<version>0.0.2</version>
</dependency>import io.github.stellnula.client.StellnulaClient;
import io.github.stellnula.client.StellnulaClientOptions;
import io.github.stellnula.config.StellnulaSnapshot;
import java.net.URI;
import java.nio.file.Path;
import java.time.Duration;
import okhttp3.OkHttpClient;
public final class StellnulaExample {
public static void main(String[] args) throws Exception {
OkHttpClient httpClient = new OkHttpClient.Builder().build();
StellnulaClientOptions options = StellnulaClientOptions.builder()
.endpoint(URI.create("http://localhost:8060"))
.appId("stellhub.core.middleware.stellcloud.admin")
.clientId("stellnula-java-sdk-demo")
.env("dev")
.namespace("default")
.group("default")
.requestTimeout(Duration.ofSeconds(5))
.snapshotDirectory(Path.of(System.getProperty("user.home"), ".stellnula", "demo"))
.build();
try (StellnulaClient client = new StellnulaClient(options, httpClient)) {
StellnulaSnapshot snapshot = client.syncNow();
System.out.println("revision=" + snapshot.revision());
client.asMap().forEach((key, value) -> System.out.println(key + "=" + value));
}
}
}try (StellnulaClient client = new StellnulaClient(options, httpClient)) {
client.start();
String serverPort = client.getRequiredValue("server.port");
System.out.println("server.port=" + serverPort);
}start() loads the local snapshot first, synchronizes with the remote server, and starts the gRPC Watch loop when watchEnabled=true.
String required = client.getRequiredValue("server.port");
int port = client.getInt("server.port").orElse(8080);
boolean enabled = client.getBoolean("feature.enabled").orElse(false);
Duration timeout = client.getDuration("http.timeout").orElse(Duration.ofSeconds(3));Given remote configuration like:
server.port=8080
server.host=127.0.0.1
server.shutdown-timeout=5sYou can bind values under a prefix:
public record ServerProperties(int port, String host, Duration shutdownTimeout) {}
ServerProperties properties = client.bindPrefix("server", ServerProperties.class);client.listenPrefix("server", event -> {
event.changes().forEach(change -> {
System.out.println(change.entry().configKey() + ": "
+ change.previousValue() + " -> " + change.currentValue());
});
});Supported listener modes:
listen(listener): listens for all changes.listenKey(key, listener): listens for a singleconfigKeyorconfigId.listenPrefix(prefix, listener): listens for a prefix.listen(predicate, listener, notifyCurrent): uses a custom predicate and optionally emits the current snapshot immediately.
Default snapshot directory:
${user.home}/.stellnula/${appId}/${env}/${cluster}
Directory layout:
${snapshotDirectory}/
├── .stellnula-snapshot.json
└── configs/
├── server.port
└── application/
└── dev.yaml
Details:
.stellnula-snapshot.jsonstores revision, checksum, and config indexes.configs/stores the actual content of each config item.- File paths prefer
configKey; unsafe paths are sanitized or moved underby-id/. - Legacy
config-snapshot.jsoncan still be read and will naturally migrate to the directory format on the next save.
| Option | Default | Description |
|---|---|---|
endpoint |
none | HTTP endpoint of stellnula-service, required |
grpcEndpoint |
server-provided or empty | Explicit gRPC Watch endpoint |
grpcPlaintext |
true |
Whether to use plaintext gRPC |
apiToken |
empty | Fixed access token |
tokenProvider |
fixed token | Dynamic token provider |
appId |
default-app |
Application identifier |
clientId |
default-client |
Client instance identifier |
env |
dev |
Environment |
region |
default |
Region |
zone |
default |
Zone |
cluster |
default |
Cluster |
namespace |
default |
Configuration namespace |
group |
default |
Default configuration group |
subscriptions |
empty list | Subscription filters |
snapshotDirectory |
${user.home}/.stellnula/${appId}/${env}/${cluster} |
Local snapshot directory |
requestTimeout |
10s | HTTP request timeout |
watchTimeout |
30s | Watch wait timeout |
retryDelay |
3s | Default retry delay |
serverRefreshInterval |
1m | Server endpoint refresh interval |
serverFailureCooldown |
30s | Failed endpoint isolation duration |
grpcShutdownTimeout |
3s | gRPC channel shutdown timeout |
watchEnabled |
true |
Whether Watch is enabled |
failFastOnBootstrap |
false |
Whether startup should fail immediately when bootstrap fails |
pageSize |
500 | Server-side page size |
maxPayloadBytes |
0 | Max payload size; 0 means unlimited |
acceptLargeFileReference |
false |
Whether to accept large-file reference delivery |
openTelemetry |
noop | Externally provided OpenTelemetry instance |
The SDK works best when frameworks or applications provide centrally managed resources:
import io.github.stellnula.client.StellnulaClient;
import io.github.stellnula.client.StellnulaClientOptions;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import okhttp3.OkHttpClient;
OkHttpClient httpClient = new OkHttpClient.Builder().build();
ExecutorService watchExecutor = Executors.newSingleThreadExecutor();
ExecutorService listenerExecutor = Executors.newFixedThreadPool(2);
StellnulaClient client = new StellnulaClient(
options,
httpClient,
watchExecutor,
listenerExecutor);When executors are provided externally, the SDK will not shut them down in close(). The caller owns their lifecycle.
The SDK only consumes the OpenTelemetry instance passed through StellnulaClientOptions.openTelemetry(OpenTelemetry). By default it uses noop. This allows applications and frameworks to export metrics consistently to Prometheus, OTLP Collector, or other backends.
Core metrics:
| Metric | Type | Description |
|---|---|---|
stellnula.client.operations |
Counter | Number of client operations |
stellnula.client.operation.duration |
Histogram | Client operation duration |
stellnula.client.errors |
Counter | Number of client errors |
stellnula.client.config.changes |
Counter | Number of config changes |
stellnula.client.snapshot.operations |
Counter | Number of local snapshot operations |
stellnula.client.snapshot.operation.duration |
Histogram | Local snapshot operation duration |
stellnula.client.listener.notifications |
Counter | Number of listener notifications |
stellnula.client.revision |
Gauge | Current config revision |
stellnula.client.config.entries |
Gauge | Number of config entries in memory |
The SDK also exposes basic management APIs for platform tooling and tests:
- Get, create, update, and delete configs.
- Replicate public configs.
- Get and maintain governance rules.
- Get, create, and end gray rules.
- Query gray-rule impact.
Management APIs share the same StellnulaClient, but production applications usually only need config read and Watch capabilities.
The repository includes a test that connects to a local stellnula-service:
mvn -q -Dtest=StellnulaClientTest#connectsLocalStellnulaServiceAndPrintsKeyValues testDefault target:
http://localhost:8060
appId=stellhub.core.middleware.stellcloud.admin
env=dev
namespace=default
group=default
Override local parameters:
mvn -q -Dtest=StellnulaClientTest#connectsLocalStellnulaServiceAndPrintsKeyValues test \
-Dstellnula.local.env=dev \
-Dstellnula.local.namespace=default \
-Dstellnula.local.cluster=default \
-Dstellnula.local.group=default \
-Dstellnula.local.token=your-tokenIf localhost:8060 is not running, the test is skipped.
- Verify that
endpointpoints to the correctstellnula-service. - Verify that
appId,env,namespace, andgroupmatch the server-side release. - Verify that the server has published the target revision.
- Verify that the local snapshot directory is readable and writable.
- Verify that the access token is valid or that
tokenProvidercan refresh it.
- Verify the server-provided or manually configured
grpcEndpoint. - Check network, firewall, and gRPC plaintext/TLS settings.
- Verify that the server revision actually changed.
- Verify that the listener is not filtered out by key or prefix.
When local metadata cannot be parsed or checksum verification fails, the SDK moves the file to .corrupt and falls back to remote synchronization. Inspect the .corrupt file to determine whether the root cause is disk corruption, manual edits, or incompatible snapshot formats.
- Do not commit local snapshot directories to source control.
- Local snapshots may contain sensitive values. Use proper operating-system file permissions.
- Prefer
StellnulaTokenProviderfor framework-managed token refresh. - Avoid logging full config values in production, especially secrets, connection strings, and certificates.
mvn clean verify
mvn -q test
mvn -q spotless:checkChanges in the following areas should include tests:
- Startup synchronization and HTTP protocol parsing.
- gRPC Watch, endpoint refresh, and failure isolation.
- Local snapshot read/write, checksum validation, and compatibility migration.
- Type conversion, prefix binding, and listeners.
- OpenTelemetry metrics.
.
├── pom.xml
├── README.md
├── README_EN.md
└── src/
├── main/java/io/github/stellnula/
│ ├── auth/
│ ├── client/
│ ├── config/
│ ├── grpc/
│ ├── internal/
│ ├── management/
│ ├── store/
│ ├── telemetry/
│ └── transport/
└── test/
snapshotDirectory(Path)is the recommended local snapshot option.snapshotFile(Path)is kept as a deprecated compatibility entry and maps to its parent directory.- The SDK core module does not depend on Spring. A future Spring Boot Starter should be implemented as a separate module.
- The SDK uses an internal shared
ObjectMapper; callers should not depend on internal JSON implementation details.
- Public API, configuration-model, or protocol-semantic changes must document compatibility impact.
- Watch, snapshot, and recovery changes must include tests.
- Behavior changes must update README or extended documentation.
- Do not introduce Spring Boot dependencies into the SDK core module.
This project declares Apache License 2.0 in pom.xml.