Skip to main content
The Java SDK targets Java 17+ and mirrors the Node.js control client. It opens a persistent TCP connection to the EventDBX control socket, negotiates Noise + Cap’n Proto frames, and exposes blocking APIs for aggregate mutations, queries, and pagination. Defaults read from EVENTDBX_* environment variables so you can drop it into Spring, Micronaut, Quarkus, or standalone JVM services.

Feature highlights

  • Blocking control-socket client with parity to eventdbxjs (create, apply, patch, archive / restore, list, events, get, select).
  • Noise XX (+ PSK) transport enabled by default; opt into plaintext with noNoise / EVENTDBX_NO_NOISE when the server allows it.
  • Publish target routing per mutation via PublishTarget.of(...) or publishTarget("plugin:mode:priority").
  • Retry/backoff, connect/request timeouts, and env-driven defaults for host, port, token, tenant, and verbosity.
  • JSON payloads/metadata handled with Jackson JsonNode so you can work with strongly typed DTOs or raw trees.

Install

Maven

<dependency>
  <groupId>com.eventdbx</groupId>
  <artifactId>eventdbx-java</artifactId>
  <version>0.1.8</version>
</dependency>

Gradle (Kotlin DSL)

dependencies {
  implementation("com.eventdbx:eventdbx-java:0.1.8")
}
The Noise handshake depends on the native snownoise library in this repo (native/snownoise). Run cargo build --release --manifest-path native/snownoise/Cargo.toml and ensure the built library is on java.library.path (or set SNOWNOISE_LIB / SNOWNOISE_LIB_PATH) before connecting. Artifacts are published to Maven Central: https://central.sonatype.com/artifact/com.eventdbx/eventdbx-java

Quickstart

import com.eventdbx.client.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.Duration;
import java.util.List;

ObjectMapper mapper = new ObjectMapper();

EventDbxConfig config = EventDbxConfig.builder()
    // host/port/token/tenant/verbose/noNoise default to EVENTDBX_* when omitted
    .retryPolicy(RetryPolicy.builder()
        .maxAttempts(3)
        .initialDelay(Duration.ofMillis(100))
        .maxDelay(Duration.ofSeconds(1))
        .build())
    .build();

try (EventDbxClient client = new EventDbxClient(config)) {
    client.connect(); // optional; operations auto-connect

    AggregateSnapshot created = client.create(
        "person",
        "p-110",
        "person_registered",
        CreateAggregateOptions.builder()
            .payload(mapper.readTree("{\"first_name\":\"Noor\",\"last_name\":\"Ali\",\"email\":\"[email protected]\"}"))
            .metadata(mapper.readTree("{\"@actor\":\"svc-directory\"}"))
            .publishTarget("search-indexer")
            .build());

    EventRecord appended = client.apply(
        "person",
        "p-110",
        "person_email_updated",
        AppendOptions.builder()
            .payload(mapper.readTree("{\"email\":\"[email protected]\"}"))
            .note("Changed by directory sync")
            .publishTarget(PublishTarget.fromString("analytics-engine:event-only"))
            .build());

    Page<AggregateSnapshot> page = client.list("person", PageOptions.builder()
        .take(50)
        .filter("person.archived = false")
        .build());

    AggregateSnapshot latest = client.get("person", "p-110");
    JsonNode projection = client.select("person", "p-110", List.of("payload.email", "metadata.@actor"));
}
EventDbxClient is AutoCloseable, and defaults will pull host/port/token/tenant from the environment. Per-call token overrides let you scope mutations to a request without rebuilding the client.

Publish targets

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;

ObjectMapper mapper = new ObjectMapper();

List<PublishTarget> targets = List.of(
    PublishTarget.of("analytics-engine"),
    PublishTarget.fromString("fraud-worker:event-only:high")
);

client.apply(
    "invoice",
    "inv-42",
    "invoice_paid",
    AppendOptions.builder()
        .payload(mapper.createObjectNode().put("status", "paid"))
        .publishTargets(targets)
        .build());
Omit publish targets to fan out to every enabled plugin. String specs follow plugin:mode:priority (mode/priority optional).

Write aggregates and events

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;

ObjectMapper mapper = new ObjectMapper();

client.create(
    "person",
    "p-110",
    "person_registered",
    CreateAggregateOptions.builder()
        .publishTarget("search-indexer")
        .build());

client.apply(
    "person",
    "p-110",
    "person_email_updated",
    AppendOptions.builder()
        .payload(mapper.createObjectNode().put("email", "[email protected]"))
        .metadata(mapper.createObjectNode().put("@actor", "svc-directory"))
        .build());

client.patch(
    "person",
    "p-110",
    "person_registered",
    List.of(new JsonPatchOperation("replace", "/last_name", null, mapper.valueToTree("Ali-Khan"))),
    PatchOptions.builder()
        .note("surname canonicalised")
        .publishTarget("analytics-engine:event-only")
        .build());

client.archive("person", "p-110", ArchiveOptions.builder().note("customer request").build());
client.restore("person", "p-110", ArchiveOptions.builder().build());
create seeds a snapshot and first event atomically, apply appends events, patch issues RFC 6902 operations against historical payloads, and archive/restore toggle write access while preserving history.

Read aggregates and events

AggregateSnapshot state = client.get("person", "p-110");

JsonNode sparse = client.select("person", "p-110", List.of("payload.email", "metadata.note"));

Page<AggregateSnapshot> firstPage = client.list("person", PageOptions.builder()
    .take(25)
    .includeArchived(false)
    .filter("person.archived = false AND person.last_name LIKE 'A%'")
    .sort("aggregateId")
    .build());

if (firstPage.nextCursor() != null) {
    Page<AggregateSnapshot> nextPage = client.list("person", PageOptions.builder()
        .cursor(firstPage.nextCursor())
        .build());
    // keep paging with nextCursor
}

Page<EventRecord> history = client.events("person", "p-110", PageOptions.builder()
    .take(100)
    .token("user-session-token") // per-request override
    .build());
Use Page.nextCursor() to resume pagination, and token on PageOptions, AppendOptions, CreateAggregateOptions, or PatchOptions to scope a call without rebuilding the client.

Retry and timeouts

  • RetryPolicy controls exponential backoff (maxAttempts, initialDelay, maxDelay); defaults to a single attempt.
  • connectTimeout and requestTimeout live on EventDbxConfig and default to 3s / 10s respectively.
  • client.isConnected() and client.disconnect() help when hot reloading or orchestrating graceful shutdowns.

Runtime configuration

VariableDefaultDescription
EVENTDBX_HOST127.0.0.1Hostname or IP address of the control socket.
EVENTDBX_PORT6363TCP port for the control plane.
EVENTDBX_TOKENemptyControl token forwarded during the handshake.
EVENTDBX_TENANT_IDemptyTenant identifier included in the initial hello.
EVENTDBX_VERBOSEfalseSet 1/true to request verbose mutation responses.
EVENTDBX_NO_NOISEfalseSet 1/true to request plaintext transport (server must allow it).
EVENTDBX_NOISE_PATTERNNoise_NNpsk0_25519_ChaChaPoly_SHA256Override the Noise pattern used during the handshake.

Noise transport

Noise is enabled by default with a PSK derived from the control token. Only disable it for controlled testing by passing noNoise(true) on EventDbxConfig or setting EVENTDBX_NO_NOISE=1; production deployments should keep Noise on. You can also override the handshake pattern with EVENTDBX_NOISE_PATTERN when experimenting with other Noise variants.

Development & testing

cargo build --release --manifest-path native/snownoise/Cargo.toml
mvn test
Run the native build once per platform so the Noise handshake can load its bindings, then execute the Maven tests or package tasks as usual.