Java Ring Buffer: A Redis-Backed Implementation

Last updated
July 17, 2026

A ring buffer is a fixed-size, FIFO data structure whose end wraps back around to its start, so that once it is full, each new element overwrites the oldest one. It is ideal for buffering data streams and keeping a rolling window of the most recent values.

This article is about the practical side: how to use a ring buffer in Java, starting with the in-process options built into common libraries, and then moving to a distributed ring buffer backed by Redis using Redisson.

Ring Buffer Options in Java

Java does not ship a dedicated ring buffer in the standard java.util collections, but you have several options.

Write your own. A ring buffer is straightforward to implement with a fixed-length array plus a head and tail index that wrap around using modulo arithmetic. This is a common interview exercise and gives you full control, but you own the concurrency and correctness details.

Apache Commons Collections — CircularFifoQueue. A bounded Queue that automatically removes the element at the head when it becomes full and you add a new one. A well-tested drop-in for most single-application needs.

Guava — EvictingQueue. Google's Guava provides a non-blocking, bounded queue that evicts the oldest element once it is full and a new one is added — effectively a ring buffer with a familiar Queue API.

ArrayDeque as a bounded buffer. java.util.ArrayDeque isn't bounded on its own, but you can wrap it to cap its size and evict from the head, which is enough for simple cases.

LMAX Disruptor. For ultra-low-latency, high-throughput inter-thread messaging, the Disruptor library is built around a ring buffer at its core. It is powerful but specialized.

All of these share one important limitation: they live inside the heap of a single JVM. The buffer isn't visible to other application instances, and its contents disappear when that instance restarts or crashes.

Why a Distributed Ring Buffer?

Modern Java applications rarely run as a single process. When you scale horizontally across several instances behind a load balancer, an in-heap ring buffer becomes a problem:

  • Each instance keeps its own separate copy, so there is no single, shared view of the buffer.
  • Any state held in that buffer is lost on deployment, restart, or failure.
  • Multiple producers and consumers running on different machines can't coordinate through it.

The usual solution is to keep the ring buffer in a shared, in-memory data store that every instance can reach. Redis is a popular choice because it is fast, in-memory, and frequently already present in a Java stack for caching and messaging. Redis on its own, however, doesn't expose Java-friendly objects — which is where a Redis Java client like Redisson comes in.

A Redis-Backed Ring Buffer in Java With Redisson

Redisson is a Redis (and Valkey) Java client that offers many familiar Java distributed objects and services, letting Java developers work with Redis through ordinary collection and queue interfaces instead of raw commands.

Redisson implements the ring buffer via the RRingBuffer interface. It is fully thread-safe, and because the buffer is stored in Redis, every application instance shares the same buffer — and, with Redis persistence and replication, it survives restarts and failover. Here is a basic example:

RRingBuffer<Integer> buffer = redisson.getRingBuffer("test");

// set a fixed capacity of 4 elements
buffer.trySetCapacity(4);

buffer.add(1);
buffer.add(2);
buffer.add(3);
buffer.add(4);

// buffer state is 1, 2, 3, 4

buffer.add(5);
buffer.add(6);

// buffer state is 3, 4, 5, 6

A few things worth noting:

  • Capacity is fixed up front. After creating the object you set its size with trySetCapacity(). Once the buffer reaches capacity, adding a new element evicts the element at the head of the queue and appends the new one at the tail — exactly the wrap-around eviction that defines a ring buffer.
  • Inspecting fullness. capacity() returns the configured size, and remainingCapacity() returns how many more elements will fit before eviction begins.
  • Standard queue semantics. RRingBuffer inherits from Redisson's RQueue (which implements java.util.Queue), so alongside add() you get peek(), poll(), remove(), and offer(), plus collection methods such as contains(), isEmpty(), and size().

Because all state lives in Redis, these operations behave consistently no matter which application instance calls them. For the full API, see the Ring Buffer section of the Redisson documentation. (Redisson also offers a newer Circular Buffer structure with slot-index and recent-value reads plus built-in aggregations — see the docs if you need those richer access patterns.)

Common Use Cases for a Distributed Ring Buffer

A shared, bounded, self-evicting buffer maps neatly onto a number of real problems in clustered applications:

  • Rolling metrics and sliding windows — keep the last N measurements (latency samples, request counts) for monitoring or rate limiting, shared across all nodes.
  • Recent-activity feeds — a capped "last 50 events" list that any instance can read and write.
  • Log and event buffering — hold a fixed backlog of recent events for consumers to drain, without unbounded memory growth.
  • Bounded history — retain only the most recent state and let older entries fall off automatically.

Getting Started

If your application runs as more than one instance and you need a ring buffer that every instance can share and that survives restarts, a Redis-backed RRingBuffer gives you that with a familiar Java Queue API and no custom concurrency code to maintain.

You can start with the open-source Redisson community edition on GitHub, or explore Redisson PRO for additional data structures, performance, and reliability features.

Similar Articles