Sitemap

Redis Data Structures in Java

5 min readApr 29, 2026

--

Understand internal encodings, time complexities, and Jedis patterns for each Redis type

You probably already use Redis as a cache, but that’s just scratching the surface. It’s a full-blown in-memory data-structure engine powering everything from leaderboards to real-time analytics, rate limiting, and stream processing. I’ll break down how to pick the right Redis structure, what actually happens in memory, and show how to use Jedis in Java — TTL, eviction, pipelining, transactions, and Lua included. If you only use Strings, you’re missing most of what makes Redis useful.

Press enter or click to view image in full size

Redis is a Data-Structure Server, not just a Cache

Redis ships with nine core data structures:

  • String
  • List
  • Hash
  • Set
  • Sorted Set
  • Stream
  • Bitmap
  • HyperLogLog
  • Geospatial index

Internally, Redis runs a single-threaded event loop (epoll or kqueue, depending on your OS) for command execution. Persistence and eviction run in the background. Every command is atomic by default. You can batch commands with pipelining, run multi-step transactions with MULTI/EXEC, or drop in Lua scripts for more advanced cases. TTL and eviction (volatile-LRU, allkeys-LFU, and friends) are per-key, mixing lazy expiration with active scans. Don’t ignore these details if you want predictable memory usage.

Which Structure for Which Problem

It’s easy to pick the wrong type and regret it later. Here’s the cheat sheet:

  • Need a simple get/set? String.
  • Queue or feed? List.
  • Membership test, uniqueness? Set.
  • Store objects or field groups? Hash.
  • Sorted/ranked data (leaderboards, scores)? Sorted Set.
  • Append-only logs, event streaming? Stream.
  • Bit flags, user activity tracking? Bitmap.
  • Fast-ish unique counts? HyperLogLog.
  • Geo queries, lat/lon proximity? Geospatial.

If you’re just hammering everything into Strings, you’re working way too hard.

Structures, Internals, and Java Examples

String

Redis uses Simple Dynamic String (SDS). For values up to 44 bytes, it uses embstr (fast, compact); above that, it's raw (slightly slower, more flexible). Reads and writes are O(1), as is length. TTL is straightforward.

try (Jedis j = pool.getResource()) {
j.set("page:hits", "42");
j.incr("page:hits");
j.expire("page:hits", 60);
}

It’s the Swiss Army knife for counters or simple values, but don’t abuse it for everything.

List

Lists are backed by QuickList (a linked list of listpacks). LPUSH and RPUSH are O(1). LRANGE is O(N). You can tune node size with list-max-ziplist-value if you care about memory vs. speed. Most people don't bother, which is fine for most workloads.

try (Jedis j = pool.getResource()) {
j.lpush("tasks", "task1", "task2");
List<String> tasks = j.lrange("tasks", 0, -1);
}

Great for queues and feeds. Not so great for random access.

Hash

Hashes are hashtables under the hood, but Redis switches to ziplist/listpack when there are ≤512 fields and the values are ≤64 bytes. This cuts memory use, but don’t expect it to scale forever.

try (Jedis j = pool.getResource()) {
Map<String,String> user = Map.of("id","1","name","Alice");
j.hset("user:1", user);
Map<String,String> data = j.hgetAll("user:1");
}

Use hashes for user profiles, session data, or any object with a handful of fields.

Set

Sets use hashtables, but if all elements are small integers, Redis quietly switches to intset. SADD and SISMEMBER are O(1). If you need uniqueness and fast membership, this is the one.

try (Jedis j = pool.getResource()) {
j.sadd("tags", "java", "redis");
boolean hasJava = j.sismember("tags", "java");
}

You won’t find a faster way to check set membership in-memory. Just don’t use it for ordered data.

Sorted Set

Sorted Sets are a hybrid: a skiplist for order, plus a dictionary for fast lookup. ZADD is O(log N), ZRANGEBYSCORE is O(log N + M). Perfect for leaderboards, ranked lists, and time-series buckets.

try (Jedis j = pool.getResource()) {
j.zadd("leaderboard", Map.of("bob",200.0,"alice",150.0));
Set<String> top10 = j.zrevrange("leaderboard", 0, 9);
}

If you’re building anything with scores or ranks, this is your go-to. Skiplist internals are why it scales.

Stream

Streams are backed by a radix tree of IDs and listpack entries. XADD is O(1), XRANGE is O(log N + M). Streams support consumer groups for fan-out. It’s not Kafka, but it’s good enough for most event logs.

try (Jedis j = pool.getResource()) {
j.xadd("events", StreamEntryID.NEW_ENTRY,
Map.of("type","login","user","42"));
List<Map.Entry<String,List<StreamEntry>>> entries =
j.xreadGroup("group1","consumerA",1,0,false,
new AbstractMap.SimpleEntry<>("events",StreamEntryID.UNRECEIVED_ENTRY));
}

Streams are underused — if you’re polling lists for logs, stop and use this.

Bitmap

Bitmaps are just strings under the hood, treated as bit arrays. SETBIT and GETBIT are O(1), but BITCOUNT is O(N). Use for flags or tracking millions of users by day. Don’t expect them to compress magically.

try (Jedis j = pool.getResource()) {
j.setbit("dailyUsers", 1001, true);
long active = j.bitcount("dailyUsers");
}

If you need to track lots of boolean states, bitmaps are hard to beat.

HyperLogLog

Redis HyperLogLog uses 16,384 registers, about 1.5 KB per key. PFADD is O(1), PFCOUNT is O(N). Don’t expect exact counts: error rate is ~0.81%. Perfect for unique user counters where precision is overrated.

try (Jedis j = pool.getResource()) {
j.pfadd("visitors","user1","user2");
long unique = j.pfcount("visitors");
}

If you see product dashboards showing “unique visitors” with no asterisk, they’re probably using this.

Geospatial

Geospatial features piggyback on sorted sets, encoding latitude/longitude into geohash scores. GEOADD is O(log N), GEORADIUS is O(log N + M). It’s a neat hack — works well for simple radius or bounding box queries.

try (Jedis j = pool.getResource()) {
j.geoadd("places",13.361389,38.115556,"Palermo");
List<GeoRadiusResponse> nearby =
j.georadius("places",15,37,200,GeoUnit.KM);
}

If you need real geo indexing, use PostGIS, but for proximity checks, this is fast and easy.

Comparison and Memory Notes

Press enter or click to view image in full size

Pipelining vs. Transactions vs. Lua

Pipelining batches of commands to cut round-trip time. It’s perfect for bulk writes — don’t use it if you need atomicity, though.

try (Jedis j = pool.getResource()) {
Pipeline p = j.pipelined();
for (int i = 0; i < 1000; i++) {
p.set("key:"+i, "value"+i);
p.expire("key:"+i, 300);
}
p.sync();
}

If you need atomicity across several commands, use MULTI/EXEC:

try (Jedis j = pool.getResource()) {
Transaction tx = j.multi();
tx.incr("counter");
tx.get("counter");
List<Object> res = tx.exec();
}

For anything conditional or to avoid race conditions, Lua scripts run on the server:

String lua = "if redis.call('get', KEYS[1])==ARGV[1] then"
+ " return redis.call('del',KEYS[1]) else return 0 end";
try (Jedis j = pool.getResource()) {
j.eval(lua, List.of("lock"), List.of("token"));
}

Understanding how SDS, quicklists, skiplists, radix trees, and probabilistic registers work lets you actually use Redis for what it’s good at. Most teams don’t bother, which is why they run into scaling surprises.

Get Mrityunjay’s stories in your inbox

Join Medium for free to get updates from this writer.

End-to-End Redis Data Structure Selection and Java Interaction

Jedis Pipelining and Command Execution Sequence

Press enter or click to view image in full size
Press enter or click to view image in full size

--

--