Skip to content

About

CockroachDB Java library (framework-free) — annotation-driven retries, Continuum.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CockroachDB Java Library

CI Release License

A pure-Java, framework-free library for CockroachDB. Its headline feature: custom annotations plus runtime dynamic proxies that make CockroachDB anomaly-handling boilerplate — the try/catch retry loops around serialization errors (SQLSTATE 40001) — disappear.

It is the framework-free sibling of the cockroachdb-spring-boot-starter, and part of a family of CockroachDB client libraries (Go, Python, .NET, Ktor). Its roadmap tracks Cockroach Continuum (Plenum, vCluster, Aegis, and an Agent/MCP surface) — see docs/CONTINUUM_PLAN.md.

Built for Java 25 (uses ScopedValue, sealed types, records, pattern-matching switch) with Gradle 9.8 (multi-module).

Architecture

Architecture

Install

Modules are served from this repository's GitHub Pages Maven repository — no token required. Each module is published as com.cockroachlabs:cockroachdb-java-library-<module> (modules: core, continuum, agent, cdc, metrics, ai, testing).

Maven

<repositories>
  <repository>
    <id>cockroachlabs</id>
    <url>https://cockroachlabs.github.io/cockroachdb-java-library/maven/</url>
  </repository>
</repositories>

<dependency>
  <groupId>com.cockroachlabs</groupId>
  <artifactId>cockroachdb-java-library-core</artifactId>
  <version>0.1.1</version>
</dependency>

Gradle

repositories {
    mavenCentral()
    maven { url 'https://cockroachlabs.github.io/cockroachdb-java-library/maven/' }
}

dependencies {
    implementation 'com.cockroachlabs:cockroachdb-java-library-core:0.1.1'
}

API reference: https://cockroachlabs.github.io/cockroachdb-java-library/

Quick start

Annotate a method, wrap the object with Crdb.enhance(...), and stop writing retry loops:

public interface AccountService {
    void transfer(long from, long to, long cents);
}

public class AccountServiceImpl implements AccountService {
    private final DataSource ds;
    public AccountServiceImpl(DataSource ds) { this.ds = ds; }

    @Transactional(retry = true)          // SERIALIZABLE + auto-retry on 40001
    public void transfer(long from, long to, long cents) {
        Crdb.tx(ds, c -> {
            // ... debit `from`, credit `to` ...
            return null;
        });
    }
}

// Assign to the INTERFACE type: interface targets are enhanced with a JDK proxy.
AccountService svc = Crdb.enhance((AccountService) new AccountServiceImpl(ds));
svc.transfer(1, 2, 5_00);   // serialization conflicts retried transparently

No interface? Crdb.enhance(pojo) generates a ByteBuddy subclass instead and copies the target's field state, so the same annotations work on concrete classes.

The always-available functional core

You never need a proxy to get retry — Crdb.tx(...) is a SERIALIZABLE, auto-retrying transaction on its own:

long balance = Crdb.tx(ds, c -> {
    try (var ps = c.prepareStatement("SELECT balance FROM accounts WHERE id = ?")) {
        ps.setLong(1, 1);
        var rs = ps.executeQuery();
        rs.next();
        return rs.getLong(1);
    }
});

Annotations

Annotation Effect
@Transactional(retry, isolation) Run in a JDBC transaction (SERIALIZABLE by default); retry on serialization conflicts using cockroach_restart semantics
@CrdbRetry(maxRetries, baseDelayMillis, maxDelayMillis) Retry on serialization anomalies with adaptive backoff
@CircuitBreaker(failureThreshold, resetTimeoutMillis) Fast-fail after repeated failures
@FollowerRead(hoursAgo, minutesAgo, secondsAgo, staleness) Read from the nearest replica with AS OF SYSTEM TIME (see below)
@Region(value) Pin execution context to a region
@VirtualCluster(value) / @Workload(Priority) Cockroach vCluster tagging: SET LOCAL application_name + workload QoS on the transaction
@AgentTool(value, destructive) Expose a method as an agent-callable tool (see the agent module)

Follower reads

CockroachDB's follower reads let any replica serve reads at a slightly stale timestamp (about 4.2s old), avoiding the round-trip to the leaseholder. @FollowerRead binds the staleness into CrdbContext; the transaction then starts with SET TRANSACTION AS OF SYSTEM TIME, so every statement reads the same snapshot from the past. A plain @FollowerRead always does a follower read — it never silently falls back to a current read.

public class CatalogServiceImpl implements CatalogService {
    private final DataSource ds;
    public CatalogServiceImpl(DataSource ds) { this.ds = ds; }

    @FollowerRead                                 // follower_read_timestamp()
    public List<Product> browseCatalog() {
        return Crdb.tx(ds, c -> /* ... read products ... */);
    }
}

CatalogService svc = Crdb.enhance((CatalogService) new CatalogServiceImpl(ds));

Choosing how far back to read. By default the read runs at follower_read_timestamp(). Use hoursAgo, minutesAgo and secondsAgo to read further back — they add up and compose to AS OF SYSTEM TIME '-Ns':

@FollowerRead(secondsAgo = 20)                    // AS OF SYSTEM TIME '-20s'
@FollowerRead(minutesAgo = 5)                     // '-300s'
@FollowerRead(hoursAgo = 1, minutesAgo = 30)      // 1h30m ago → '-5400s'

If any of hoursAgo/minutesAgo/secondsAgo is non-zero they take precedence over staleness; when all three are zero, the raw staleness expression is used (defaulting to follower_read_timestamp()). The interval is built only from the validated numbers, never from caller text.

Fails fast instead of reading current data. A negative hoursAgo, minutesAgo or secondsAgo throws IllegalStateException before the method runs, naming the offending annotation — never a silent current read.

Limitation (v1): the core library emits AS OF SYSTEM TIME only when the method actually runs inside a managed transaction — Crdb.tx(...), Crdb.template(ds), or a @Transactional method. A bare @FollowerRead method that opens no transaction binds the staleness but has nothing to apply it to, so no follower read happens on the wire. Unlike the Spring starter, which opens its own read-only REQUIRES_NEW transaction, the core does not auto-open one. Pair @FollowerRead with a transaction.

Cockroach Continuum

vCluster — workload tagging

@VirtualCluster / @Workload tag a transaction's session: the vCluster name becomes application_name and the workload priority maps to default_transaction_quality_of_service (HIGH→critical, NORMAL→regular, LOW→background), applied with SET LOCAL so pooled connections stay clean.

@VirtualCluster("analytics")
@Workload(Priority.LOW)
public List<Row> nightlyRollup() { return Crdb.tx(ds, c -> ...); }

// or via the facade:
Crdb.vcluster("analytics").workload(Priority.LOW).tx(ds, c -> ...);

v1 does workload tagging/QoS; routing to a distinct vCluster endpoint is platform-dependent and out of scope. Cockroach vCluster hosts cannot use Cockroach Plenum — requesting Crdb.storage(...) under a vCluster context logs a warning.

Plenum — storage insights (read-only)

for (TableStorage t : Crdb.storage(ds).summary()) {
    System.out.printf("%s.%s rows=%s bytes=%s%n",
        t.schema(), t.table(), t.rows(), t.bytes());   // OptionalLong, may be empty
}

Customer-safe SQL only (SHOW TABLES / SHOW RANGES ... WITH TABLES, DETAILS); missing columns degrade to OptionalLong.empty() instead of throwing.

Aegis — findings + exhaust (feature-flagged)

AegisClient aegis = Crdb.aegis(AegisConfig.defaults());  // simulated backend
aegis.findings();
aegis.emit(AppEvent.of("deploy", "app", Map.of("version", "1.2.0"), System.currentTimeMillis()));

// Feed retry/circuit signals into Aegis automatically:
new AegisExhaust(aegis).register();

AegisConfig.defaults() (or any config without an endpoint) uses the in-memory simulated backend; a real HTTP client is stubbed until the Aegis API is public.

Agent / MCP tool surface

CrdbAgent agent = CrdbAgent.from(new CrdbOpsTools(ds));
String tools  = agent.listTools();              // MCP tools/list JSON
String result = agent.invoke("retry_stats", "{}");  // MCP tools/call JSON
// destructive tools are hidden until: agent.allowDestructive(true)

Cloud control plane + virtual clusters

The continuum module adds a Cloud control-plane client (simulated until an API key is set), typed metric-export builders, a virtual-cluster lifecycle manager, and a health probe.

import com.cockroachlabs.crdb.continuum.cloud.*;

CloudClient cloud = ContinuumCloudFactory.make(new CloudClient.Config(true, "", apiKey));
cloud.listClusters();
cloud.configureMetricExport(clusterId, MetricExport.datadog(ddKey, "US1")); // datadog/cloudWatch/prometheus
boolean ready = CloudHealth.isReachable(cloud);

VirtualClusterManager vm = VirtualClusterManager.forDataSource(ds);
vm.create("analytics");
String dsn = VirtualClusterManager.routingConnString(baseUrl, "analytics"); // adds -ccluster option

MCP client + Aegis over MCP

MCP is a public, versioned protocol, so the client speaks the real wire format (2025-06-18 initialize handshake, tools/list, tools/call, SSE + session handling). The Aegis REST API is not yet public, so Aegis is reached read-only over MCP with caller-supplied tool names (discover them via listTools()) rather than an invented wire protocol.

import com.cockroachlabs.crdb.continuum.mcp.*;
import com.cockroachlabs.crdb.continuum.aegis.McpAegisClient;

McpClient mcp = ContinuumMcpFactory.create(
        new McpClient.Config(true, "https://cockroachlabs.cloud/mcp", token));
mcp.listTools();
McpAegisClient aegis = new McpAegisClient(mcp,
        new McpAegisClient.ToolNames("list_findings", "list_recommendations", "emit_event"));
aegis.findings();

Modules

Module Status Purpose
core implemented Annotations, sealed anomalies + classifier, adaptive retry, circuit breaker, ScopedValue context, transaction template, dynamic-proxy engine, event hooks, Crdb facade
continuum implemented Cockroach Plenum storage insights, vCluster tagging/QoS, Aegis client (simulated) + exhaust
agent implemented Agent / MCP tool surface consuming @AgentTool
cdc stub (P4) Changefeed webhook handling + test harness
metrics stub (P4) Micrometer / OpenTelemetry instrumentation
ai stub (P4) pgvector similarity + semantic search
testing stub (P4) Testcontainers harness + concurrency runner

Build & test

./gradlew build            # compile all modules + run tests
./gradlew :core:test       # core tests only

Integration tests connect to a local CockroachDB. Override the target with the CRDB_URL environment variable (default jdbc:postgresql://localhost:26257/defaultdb?user=root&sslmode=disable). If the cluster is unreachable, integration tests are skipped, not failed.

Requirements

  • JDK 25 (toolchain-enforced)
  • Gradle 9.8+ (wrapper included; Gradle 8.x cannot run on JDK 25)
  • A CockroachDB cluster for integration tests

Related

About

CockroachDB Java library (framework-free) — annotation-driven retries, Continuum.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages