lakestream
Clients & Libraries

Java

lakestream-api and ursa-storage-lakestream: the embeddable Java library.

Two Java modules make up the embeddable library — one you program against, one you actually depend on. Both are plain JVM code you link into your own process: no broker to run, no protocol translation layer between your calls and the storage underneath.

lakestream-api

lakestream-api is the protocol-neutral interface module: StreamCatalog, Stream, StreamWriter, StreamReader, Log, and LogCursor, defined as interfaces and records with no implementation of its own. It's the same module the specification documents level by level — nothing in it knows about Kafka, Pulsar, or any other protocol, which is what lets more than one broker implementation share it. As an embedder, you don't have to program against all of it: StreamCatalog and Stream (Level 2) cover namespaces, streams, and routing; Log and LogCursor (Level 0) are the per-log primitives underneath, usable directly if your service just needs a durable, appendable log with no catalog on top.

ursa-storage-lakestream

ursa-storage-lakestream implements lakestream-api: IndexedStreamCatalog for catalog operations, LogImpl and LogCursorImpl per log, and the stream layouts that route a write to a log. This is the module external consumers embed directly — not ursa-storage-core underneath it, which is internal to Ursa.

Every handle in the API — catalog, stream, writer, reader, log, and cursor — extends AutoCloseable and is documented safe for concurrent use, so a service embedding it can pool and share handles the way it would any other JVM resource.

That's not theoretical. Ursa for Kafka's own build pins io.streamnative:ursa-storage-ml at 4.1.3.2, and ursa-storage-ml depends on ursa-storage-lakestream for its stream and cursor logic rather than reimplementing it (see Ursa's architecture) — so a running diskless Kafka broker is, transitively, an ursa-storage-lakestream consumer today, under the same io.streamnative groupId and version line this module ships under — even though Ursa itself is developed at 5.0.0-M1, ahead of the 4.1.x artifacts this build consumes.

Getting the artifacts

Building from source is the dependable path today — see the Ursa quickstart for the Maven build. That build installs ursa-storage-lakestream, and everything it depends on, to your local Maven repository — enough to declare it as a normal dependency in your own service's build. The coordinates a downstream build consumes look like this:

<dependency>
  <groupId>io.streamnative</groupId>
  <artifactId>ursa-storage-lakestream</artifactId>
  <version>VERSION</version>
</dependency>

Build from source today

VERSION above is a placeholder, not a release to pin — these coordinates aren't yet resolvable from a public package repository. That's settling as the project opens up; building from source is the dependable path until it does.

A sketch of the API shape

This is a sketch, not a runnable tutorial: the shape of a catalog/stream/write/read round trip, using only real members of the API, elided (...) wherever a real program would supply its own wiring.

StreamCatalog catalog = ...;                     // e.g. IndexedStreamCatalog
StreamIdentifier id = StreamIdentifier.of("public/default", "events");
Partitioning partitioning =
    new Partitioning(PartitioningStrategy.INDEXED, Map.of("numPartitions", "1"));
ByteBuf payload = ...;                            // event bytes

catalog.createStream(id, new StreamConfig(), partitioning, new SchemaConfig(), Map.of())
    .thenCompose(stream -> stream.writer().write(RoutingKey.roundRobin(), 1, payload))
    .thenCompose(written -> catalog.loadStream(id).thenApply(Stream::reader)
        .thenCompose(reader -> reader.read(written.logId(), written.offset(), 1, 1_000_000)))
    .thenAccept(result -> result.entries().forEach(entry -> System.out.println(entry.offset())));

createStream resolves to a Stream handle; writer() and reader() come off that handle rather than off the catalog directly. The write result carries the LogId and offset it landed at, which the read call above reuses to fetch the same entry back — a real consumer would more often track read position through a LogCursor than round-trip a single write like this. Every I/O operation in the API — create, write, read, append — returns a CompletableFuture; composing chains like this, rather than calling .get() or .join() mid-chain, is the intended style throughout.

Where next

  • Stream Catalog — the full StreamCatalog/Stream surface this sketch only samples.
  • Log storage — the Log/LogCursor primitives underneath it.