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