New release

LiveObjects comes to Java: navigating shared state with the path API

LiveObjects now supports Java, and the path-based model that reshaped the JavaScript API is built in from the start. Instead of holding a reference to a specific object, PathObject lets you point at a location in the tree, so a reference keeps working even after the object underneath gets replaced.

LiveObjects comes to Java: navigating shared state with the path API

Shared state is a hard problem in the unglamorous sense: the concepts are well understood, but someone still has to build the conflict resolution, the reconnection handling, and the state recovery before anyone can ship the feature that depends on it. That's what LiveObjects is for, and the path-based model that reshaped the API in JavaScript is now available to Java developers too.

Point at a location, not an object

Hold a reference to a specific LiveMap or LiveCounter instance, and that reference only holds up until the object gets replaced. When it does, a subscription bound to the old instance goes quiet, and any code still holding that reference is now working with stale data instead of what's actually there.

PathObject sidesteps that by describing a location in the object tree rather than a specific value living there. Call channel.object.get() once, keep the reference, and let it resolve to whatever's actually at that path each time you use it:

LiveMapPathObject root = channel.object.get().join();

PathObject visits = root.get("visits");
visits.asLiveCounter().increment(5).join();

// Someone replaces the LiveCounter instance stored at 'visits'
root.set("visits", LiveMapValue.of(LiveCounter.create(100))).join();

// The same reference still works, against the new instance
visits.asLiveCounter().increment(1).join();

No rebinding, no stale subscription. The reference outlives whatever object happens to be sitting at that path today.

Reading and writing through a path

A PathObject doesn't carry a type until you ask for one, so you infer the type at the point you use it, by calling asLiveCounter(), asLiveMap(), asString(), and so on:

String theme = root.at("settings.theme").asString().value();
System.out.println(theme); // e.g. "dark"
root.get("settings").asLiveMap().set("theme", LiveMapValue.of("light")).join();

These casts never throw, even against the wrong type. A wrong guess returns null on a read, or fails the returned CompletableFuture on a write, so a quick getType() or exists() check is there for the moments you're not sure what's stored at a path yet.

Everything else about working with data reads the way you'd hope: entries(), keys(), and values() walk a LiveMap, and compactJson() gives you a serializable snapshot of an entire subtree, cycles and binary data included, in one call.

What this looks like in practice

What matters more than the API surface is what a Java backend can now do without owning a coordination layer: track a live leaderboard, hold session state for an AI agent, or sync a shared config panel, using a path as the only thing it needs to remember. "Increment whatever counter is at leaderboard.alice" or "read whatever's at session.progress" is a complete instruction. The SDK deals with whatever happens to the object underneath.

Ably's example app shows the same shape in a single app: a color voting feature backed by a LiveCounter and a collaborative to-do list backed by a LiveMap, with every client reading and writing through paths rather than shared object instances.

A server writing session state for an AI agent looks like this:

PathObject session = root.get("session");
session.asLiveMap().set("current_task", LiveMapValue.of("Summarizing document")).join();

An Android client subscribing to that same path and rendering whatever's there:

suspend fun subscribeToSession(channel: Channel) {
    val root: LiveMapPathObject = channel.`object`.get().await()
    val session = root.get("session").asLiveMap()    
    val currentTask = session.get("current_task").asString()
    currentTask.subscribe {
        updateAgentStatusPanel("currentTask", currentTask.value())
    }
}

Get started

The path API ships as part of the standard Objects plugin, so there's no separate install step beyond adding the plugin alongside the core Pub/Sub SDK. The LiveObjects Java quickstart walks through that setup, channel modes, and the full method surface, and the PathObject reference is the place to go for everything covered here in more depth.