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).
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).
<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>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/
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 transparentlyNo interface? Crdb.enhance(pojo) generates a ByteBuddy subclass instead and
copies the target's field state, so the same annotations work on concrete
classes.
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);
}
});| 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) |
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 TIMEonly when the method actually runs inside a managed transaction —Crdb.tx(...),Crdb.template(ds), or a@Transactionalmethod. A bare@FollowerReadmethod 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-onlyREQUIRES_NEWtransaction, the core does not auto-open one. Pair@FollowerReadwith a transaction.
@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.
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.
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.
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)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 optionMCP 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();| 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 |
./gradlew build # compile all modules + run tests
./gradlew :core:test # core tests onlyIntegration 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.
- JDK 25 (toolchain-enforced)
- Gradle 9.8+ (wrapper included; Gradle 8.x cannot run on JDK 25)
- A CockroachDB cluster for integration tests
cockroachdb-spring-boot-starter— Spring Boot auto-configuration siblingcockroachdb-go-library,cockroachdb-python-library,cockroachdb-dotnet-library,cockroachdb-ktor-dsldocs/CONTINUUM_PLAN.md— Cockroach Continuum feature roadmap