Skip to main content

Java Client

The official Java client library for rstmdb.

Repository: github.com/rstmdb/rstmdb-java

Installation​

Gradle (Kotlin DSL)​

dependencies {
implementation("com.rstmdb:rstmdb-client:0.1.0")
}

Gradle (Groovy)​

dependencies {
implementation 'com.rstmdb:rstmdb-client:0.1.0'
}

Maven​

<dependency>
<groupId>com.rstmdb</groupId>
<artifactId>rstmdb-client</artifactId>
<version>0.1.0</version>
</dependency>

Requirements: Java 11+

Features​

  • Full RCP protocol: RCPX binary framing with CRC32C Castagnoli checksums
  • Both async (CompletableFuture<T>) and sync (*Sync) APIs for all operations
  • All 22 operations and 16 error codes
  • TLS/mTLS support via SSLContext
  • Streaming subscriptions via Iterable
  • Builder-pattern configuration with RstmdbOptions
  • Testcontainers module for integration testing

Quick Start​

import com.rstmdb.client.*;
import com.rstmdb.client.model.*;
import java.util.List;
import java.util.Map;

try (var client = RstmdbClient.connect("localhost", 7401)) {
// Define a state machine
client.putMachineSync(new PutMachineRequest(
"order", 1,
new MachineDefinition(
List.of("pending", "paid", "shipped", "delivered"),
"pending",
List.of(
new Transition(List.of("pending"), "PAY", "paid", null),
new Transition(List.of("paid"), "SHIP", "shipped", null),
new Transition(List.of("shipped"), "DELIVER", "delivered", null)
),
null
),
null
));

// Create an instance
var inst = client.createInstanceSync(new CreateInstanceRequest(
"order-001", "order", 1,
Map.of("customer", "alice", "total", 99.99),
null
));
System.out.println("Created: " + inst.getInstanceId() + " in state " + inst.getState());

// Apply events
var result = client.applyEventSync(new ApplyEventRequest(
"order-001", "PAY",
Map.of("payment_id", "pay-123"),
null, null, null, null
));
System.out.println(result.getFromState() + " -> " + result.getToState());
}

Connection​

Basic Connection​

var client = RstmdbClient.connect("localhost", 7401);

With Authentication​

var opts = RstmdbOptions.builder()
.auth("my-secret-token")
.build();

var client = RstmdbClient.connect("localhost", 7401, opts);

TLS Connection​

var opts = RstmdbOptions.builder()
.auth("my-secret-token")
.sslContext(RstmdbOptions.createTlsContext(Path.of("ca.pem")))
.build();

var client = RstmdbClient.connect("secure.example.com", 7401, opts);

Development Mode (Insecure)​

// Skip TLS verification - development only!
var opts = RstmdbOptions.builder()
.sslContext(RstmdbOptions.insecureTlsContext())
.build();

var client = RstmdbClient.connect("localhost", 7401, opts);

Configuration Options​

PropertyTypeDefaultDescription
authStringnullBearer token for authentication
sslContextSSLContextnullTLS configuration (null = plain TCP)
connectTimeoutDuration10sConnection dial timeout
requestTimeoutDuration30sPer-request timeout
clientNameStringnullClient name sent in HELLO handshake

API Reference​

All operations are available as both async (CompletableFuture<T>) and sync (*Sync) methods.

Machine Operations​

PutMachine​

Register a state machine definition.

var result = client.putMachineSync(new PutMachineRequest(
"order", 1,
new MachineDefinition(
List.of("pending", "paid", "shipped"),
"pending",
List.of(
new Transition(List.of("pending"), "PAY", "paid", null),
new Transition(List.of("paid"), "SHIP", "shipped", null)
),
null
),
null
));

GetMachine​

Retrieve a machine definition.

var machine = client.getMachineSync("order", 1);
System.out.println("Initial: " + machine.getDefinition().getInitial());

ListMachines​

List all machines.

var machines = client.listMachinesSync();
for (var m : machines.getItems()) {
System.out.println(m.getMachine() + ": " + m.getVersions());
}

Instance Operations​

CreateInstance​

Create a new instance.

var inst = client.createInstanceSync(new CreateInstanceRequest(
"order-001", "order", 1,
Map.of("customer", "alice"),
null
));

GetInstance​

Get instance state and context.

var inst = client.getInstanceSync("order-001");
System.out.println("State: " + inst.getState());
System.out.println("Context: " + inst.getCtx());

ListInstances​

List instances with optional filters.

var list = client.listInstancesSync(
ListInstancesOptions.builder()
.machine("order")
.state("paid")
.limit(50)
.build()
);
for (var inst : list.getInstances()) {
System.out.println(inst.getId() + ": " + inst.getState());
}

DeleteInstance​

Delete an instance.

var result = client.deleteInstanceSync("order-001");
System.out.println("Deleted: " + result.isDeleted());

Event Operations​

ApplyEvent​

Apply an event to trigger a state transition.

var result = client.applyEventSync(new ApplyEventRequest(
"order-001", "PAY",
Map.of("amount", 99.99),
null, null, null, null
));

System.out.println("From: " + result.getFromState());
System.out.println("To: " + result.getToState());

With optimistic concurrency:

var result = client.applyEventSync(new ApplyEventRequest(
"order-001", "PAY",
Map.of("amount", 99.99),
"pending", // expectedState
null, null, null
));

Batch​

Execute multiple operations in a single request.

var results = client.batchSync(BatchMode.ATOMIC, List.of(
BatchOperation.createInstance(new CreateInstanceRequest(
"order-002", "order", 1, Map.of(), null
)),
BatchOperation.applyEvent(new ApplyEventRequest(
"order-002", "PAY", Map.of(), null, null, null, null
))
));

for (var r : results) {
System.out.println("status=" + r.getStatus());
}

Streaming​

WatchAll​

Subscribe to events with filtering.

var sub = client.watchAllSync(new WatchAllOptions(
true, null, new String[]{"order"}, null, null, null
));

for (var event : sub.events()) {
System.out.printf("%s: %s -> %s (event: %s)%n",
event.getInstanceId(), event.getFromState(),
event.getToState(), event.getEvent());
}

WatchInstance​

Watch a specific instance.

var sub = client.watchInstanceSync("order-001", true);

for (var event : sub.events()) {
System.out.printf("Event: %s, New state: %s%n",
event.getEvent(), event.getToState());
}

System Operations​

Ping​

Health check.

client.pingSync();

Info​

Get server information.

var info = client.getInfoSync();
System.out.println("Server: " + info.getServerName() + " " + info.getServerVersion());

WAL Operations​

WalRead​

Read entries from the write-ahead log.

var result = client.walReadSync(0, 100);
for (var record : result.getRecords()) {
System.out.println("offset=" + record.getOffset() + " entry=" + record.getEntry());
}

WalStats​

Get WAL statistics.

var stats = client.walStatsSync();
System.out.println("Entries: " + stats.getEntryCount() + ", Size: " + stats.getTotalSizeBytes() + " bytes");

Compact​

Trigger WAL compaction.

var result = client.compactSync(false);
System.out.println("Reclaimed: " + result.getBytesReclaimed() + " bytes");

Async Usage​

All sync methods have async counterparts returning CompletableFuture<T>:

client.ping()
.thenCompose(v -> client.createInstance(request))
.thenCompose(inst -> client.applyEvent(eventRequest))
.thenAccept(result -> System.out.println(result.getToState()))
.exceptionally(ex -> {
if (ex.getCause() instanceof RstmdbException re) {
System.err.println("Error: " + re.getErrorCode());
}
return null;
})
.join();

Error Handling​

try {
client.applyEventSync(request);
} catch (RstmdbException e) {
if (RstmdbException.isInstanceNotFound(e)) {
System.out.println("Instance not found");
} else if (RstmdbException.isInvalidTransition(e)) {
System.out.println("Cannot apply event from current state: " + e.getMessage());
} else if (RstmdbException.isConflict(e)) {
System.out.println("Optimistic concurrency conflict");
} else if (e.isRetryable()) {
System.out.println("Transient error, safe to retry");
}
}

Error codes: UNSUPPORTED_PROTOCOL, BAD_REQUEST, UNAUTHORIZED, AUTH_FAILED, NOT_FOUND, MACHINE_NOT_FOUND, MACHINE_VERSION_EXISTS, MACHINE_VERSION_LIMIT_EXCEEDED, INSTANCE_NOT_FOUND, INSTANCE_EXISTS, INVALID_TRANSITION, GUARD_FAILED, CONFLICT, WAL_IO_ERROR, INTERNAL_ERROR, RATE_LIMITED.

Examples​

Order Processing​

import com.rstmdb.client.*;
import com.rstmdb.client.model.*;
import java.util.List;
import java.util.Map;

public class OrderProcessing {
static void processOrder(RstmdbClient client, String orderId) {
// Create order
client.createInstanceSync(new CreateInstanceRequest(
orderId, "order", 1,
Map.of("items", List.of("item-1", "item-2"), "total", 149.99),
null
));

// Process payment
client.applyEventSync(new ApplyEventRequest(
orderId, "PAY",
Map.of("payment_id", "pay-123"),
null, null, null, null
));

// Ship order
client.applyEventSync(new ApplyEventRequest(
orderId, "SHIP",
Map.of("tracking", "1Z999"),
null, null, null, null
));

// Get final state
var order = client.getInstanceSync(orderId);
System.out.println("Order " + orderId + " is now: " + order.getState());
}

public static void main(String[] args) throws Exception {
try (var client = RstmdbClient.connect("localhost", 7401)) {
processOrder(client, "order-001");
}
}
}

Event Consumer​

import com.rstmdb.client.*;

public class EventConsumer {
public static void main(String[] args) throws Exception {
try (var client = RstmdbClient.connect("localhost", 7401)) {
System.out.println("Listening for shipped orders...");

var sub = client.watchAllSync(new WatchAllOptions(
true, null, new String[]{"order"},
null, new String[]{"shipped"}, null
));

for (var event : sub.events()) {
System.out.println("Order " + event.getInstanceId() + " shipped!");
// Send notification, update external system, etc.
}
}
}
}

Testcontainers​

The rstmdb-testcontainer module provides JUnit integration for testing:

// Gradle
testImplementation("com.rstmdb:rstmdb-testcontainer:0.1.0")
@Testcontainers
class OrderServiceTest {
@Container
static RstmdbContainer rstmdb = new RstmdbContainer();

@Test
void testOrderWorkflow() throws Exception {
try (var client = RstmdbClient.connect(
rstmdb.getHost(), rstmdb.getPort())) {
// test code here
}
}
}

Resources​