Skip to content

Data Structures

This page covers the distributed data structures that LoomCache provides: maps, queues, sets, topics, CRDTs, and CP primitives. The client SDK exposes each structure through a dedicated Loom* wrapper obtained from LoomClient (or, for CP primitives, from client.consistencySubsystem()).

For the production-supported single-group path, every committed write flows through Raft: replicated to a majority of nodes, persisted to the write-ahead log (WAL), and applied atomically to the state machine before the client response is sent. Multi-group sharding remains unsupported and fail-closed in production until each group has independent WAL, Raft metadata, snapshot, install-snapshot, restart recovery evidence, durable migration chunk ACKs, consensus-backed ownership cutover, and an explicit sharding release gate.

Distributed Data Structures

A dozen-plus distributed primitives — maps, queues, topics, CRDTs, and CP — replicated over Raft.

"user:101"{ name: "Alice" }
"config:rateLimit"5000
"session:temp"Writing...
TTL: 30s

LoomMap is the primary key-value structure:

LoomMap<String, String> users = client.getMap("users");
users.put("alice", "Alice");
String name = users.get("alice"); // near cache may answer locally
users.putIfAbsent("bob", "Bob"); // atomic
users.delete("alice");

Supported operations: get, put, putTransient, putIfAbsent, putAll, putWithTtl, delete, evict, containsKey, size, getAll, getAndRemove, getEntryView, computeIfAbsent, replace(K, V, V) (compare-and-swap), compareAndDelete(K, V), removeAll, clear(), cursor-based scan / scanner, query helpers (keySet, entrySet, values, keyset pagination, projections, and aggregates), addEntryListener / removeEntryListener, executeOnKey, executeOnKeys, executeOnEntries, MapStore loadAll, and async variants (getAsync, putAsync, …). Stats are available via LoomMap.getStats().

Reads are linearizable by default: leaders use lease/read-index checks and followers redirect to the leader. Non-production embedded/member-local compatibility deployments can opt into readBackupData(true) to serve reads from the local replica. This trades latency for possible staleness during replication lag or leader failover. loomcache.profile=production rejects ClusterConfig.readBackupData=true, so production reads stay on the linearizable leader/read-index path. The property key depends on the deployment mode: the standalone server reads loomcache.cluster.read-backup-data (legacy alias: loomcache.read-backup-data); the Spring Boot module binds loomcache.server.read-backup-data.

Embedded/server maps expose MapStore/EntryStore (and the generalized DataStore/DataLoader) integration points. Packaged generic JDBC MapStore and Spring JPA write-through configuration surfaces are fail-closed under the production profile. The Spring default-map JPA write-through bridge is not installed in production; unsafe/local optional datasources still fail preflight when that bean is present. The production-eligible external-store path is a custom MapStore/DataStore wired programmatically through a clustered server extension. That path provides leader-owned external writes, snapshot/graceful-drain recovery for write-behind queues, and the leader-owned read-through fill path. Client-facing MapStore helpers are scoped to that same path: loadAll(...) is leader-owned and Raft-replicated, while putTransient(...) deliberately bypasses external write-through for memory-only replicated values.

Per-map eviction is configured through map configuration using a maxSize entry-count ceiling and one of five EvictionPolicy values: LRU, LFU, FIFO, RANDOM, or NONE (default). The server-wide memory ceiling is a separate concept: loomcache.eviction.max-memory-bytes (standalone server) / loomcache.server.eviction.max-memory-bytes (Spring Boot) applies a shared byte budget across all maps on a node. These two levels are independent — per-map limits entry count, server-wide limits total heap footprint.

This release fail-closes server-side LRU, LFU, FIFO, and RANDOM eviction together with maximum idle expiration: neither per-map eviction nor server-wide memory eviction is production-supported until eviction victim selection is Raft-applied and proven through WAL/snapshot/restart tests. Configuring either in the production profile raises a startup error.

LoomQueue provides distributed queue operations:

LoomQueue<String> tasks = client.getQueue("tasks");
tasks.offer("send-email");
String next = tasks.poll();
String peek = tasks.peek();

Supported operations: offer, poll, poll(Duration), take(), peek, size, offerAll, bounded poll(int), drain, drainTo, and async variants. Embedded QueueStore exists as an SPI, but snapshot/restart parity is not production-supported until queue restore, rollback, and duplicate/lost item failure windows are release-validated.

LoomSet provides distributed set operations:

LoomSet<String> tags = client.getSet("tags");
tags.add("java");
tags.contains("cache");
tags.size();

Supported operations: add, remove, contains, size, clear, cursor-based scan, and async variants.

LoomTopic provides typed publish/subscribe over a client-managed polling path:

LoomTopic<String> events = client.getTopic("events", String.class);
int sub = events.subscribe(msg -> System.out.println("got: " + msg));
events.publish("hello");
events.unsubscribe(sub);

subscribe(...) returns an integer subscription id. Pass that id to LoomTopic.unsubscribe(int) to remove one polling subscription, or call closeSubscriptions() to drop them all. Do not assume Hazelcast-style push dispatch semantics.

LoomAtomicLong is a distributed atomic counter:

LoomAtomicLong counter = client.consistencySubsystem().getAtomicLong("hits");
counter.incrementAndGet();
long cur = counter.get();
counter.compareAndSet(cur, cur + 10);

All operations — get, set, arithmetic, and compare-and-set — are fully Raft-replicated.

LoomLinearizableLock / LoomLinearizableSemaphore (CP)

Section titled “LoomLinearizableLock / LoomLinearizableSemaphore (CP)”

Obtain CP locks and semaphores from the consistency subsystem:

LoomLinearizableLock lock = client.consistencySubsystem().getLock("orders");
LoomLinearizableSemaphore permits = client.consistencySubsystem().getSemaphore("pool");

Production servers accept lock and semaphore mutations only when the request carries an active Raft-managed CP session. The Java client manages that session lifecycle internally for LoomLinearizableLock and LoomLinearizableSemaphore; direct session operations are internal to the client/server contract, not a public application workflow. A sessionless semaphore mutation still fails closed in production with an unsupported-operation response.

Other CP extension handles are non-production only and their direct requests are rejected under the production profile. Use the in-process ConsistencySubsystem for non-production embedded scenarios.

LoomBatch groups multiple operations into a single request:

// Non-atomic (default): execute() sends one server request; executeAndReport() can split keyed map owner groups.
client.batch()
.map("users").put("alice", "Alice")
.map("users").delete("eve")
.execute();
// Atomic: single-owner batches use one mutation boundary; cross-owner keyed map put/delete uses server 2PC.
client.batch()
.atomic()
.map("users").put("alice", "Alice")
.map("users").delete("eve")
.execute();
// Replicated: implies atomic=true and requests cross-replica durability for the batch.
client.batch()
.replicated()
.map("users").put("alice", "Alice")
.execute();

Atomicity is opt-in: by default (atomic=false), execute() sends one server-side request and sharded servers fail closed for unsafe mixed-owner batches. Use executeAndReport() when you want keyed map put/delete/putIfAbsent operations split into owner-group sub-batches with explicit partial-outcome reporting. Sharded batch execution currently accepts only keyed map put/delete/putIfAbsent operations. Set/queue batches are supported only on the non-sharded/single-Raft-group path; in sharded mode any batch containing set or queue operations fails closed. Call .atomic() for all-or-nothing server handling: single-owner batches use one mutation boundary, and cross-owner sharded batches currently support keyed map put/delete through the server 2PC path. Call .replicated() to additionally request cross-replica durability, which implies atomic=true.

Beyond the structures shown above, the SDK ships a dedicated Loom* wrapper for every server-side structure. Obtain them from LoomClient (or client.consistencySubsystem() for CP primitives):

  • getMultiMapLoomMultiMap, getListLoomList, getPriorityQueueLoomPriorityQueue, getRingbufferLoomRingbuffer. The client API exposes add(E), readOne(long), readMany(long, int), headSequence(), tailSequence(), and capacity(). Server-side LoomFunction filtering is an internal implementation detail — LoomRingbuffer.readMany has no filter overload on the client API. RingbufferStore supports embedded persistence.
  • getReplicatedMapLoomReplicatedMap — AP, eventually-consistent broadcast map (put, get, remove, containsKey, size) with per-key last-writer-wins and a background anti-entropy sweep; not linearizable. Writes are fail-closed under loomcache.profile=production until broadcast convergence is chaos-certified (-Dloomcache.replicatedmap.allow-production=true opts in for non-production use); reads are unrestricted.
  • getReliableTopicLoomReliableTopic — ringbuffer-backed message store with monotonic sequence numbers.

LoomReliableTopic is a pull model, not a push/subscribe model:

  • publish(T) — appends a message and returns the assigned sequence number.
  • publish(T, TopicOverloadPolicy) — overload that explicitly specifies the overload policy (DISCARD_OLDEST, DISCARD_NEWEST, BLOCK, or ERROR) for this publish call. ERROR rejects the publish with an exception when the ringbuffer is full.
  • readFrom(long sequence, int maxCount) — fetches up to maxCount messages starting at sequence.
  • unsubscribe() — the no-argument LoomReliableTopic.unsubscribe() method always throws UnsupportedOperationException because reliable topics have no server-side push subscription. This is separate from LoomTopic.unsubscribe(int), which removes a client-managed polling subscription by id.

Default capacity is 10,000 messages per topic (configurable at construction time).

  • getIdGeneratorLoomIdGenerator (SnowflakeIdGenerator on the server).
  • CRDTs: getPNCounter/getGSet/getORSet/getLWWRegisterLoomPNCounter/LoomGSet/LoomORSet/LoomLWWRegister. The server-side CRDT state is managed by the data-structure registry.
  • CP primitives: LoomLinearizableLock, LoomLinearizableSemaphore, LoomAtomicLong. Production posture is as described under LoomLinearizableLock / LoomLinearizableSemaphore above: the Java client manages lock/semaphore CP sessions internally; other CP extension handles are non-production only.

Two further Loom* wrappers are not data structures but share the same client surface:

  • getExecutorService(name)LoomExecutorService — distributed task execution.
  • newTransaction() / newTransaction(Duration)LoomTransaction — a multi-key transactional unit (see Client API › Transactions).

ContinuousQueryCache (server-side filtered map view that auto-updates via change events) is exposed client-side as LoomQueryCache in com.loomcache.client.query.

LoomCache is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Hazelcast, Inc. or by any other company whose products are named in this documentation. “Hazelcast” is a trademark of Hazelcast, Inc.; references to it are nominative and describe only migration and comparison. All other product and company names are trademarks of their respective owners and are used for identification purposes only.