a map of backend systems
◀ Back to the map

Redis line

The data types as a design toolkit

Redis isn't a flat map of strings — it's a server of data structures, and picking the right one is a modeling decision.


Redis deep-dive · Part 2 of 10. Previous: Redis, from the ground up. Next: Expiration, eviction & memory.

The name says key–value store, and that framing quietly costs people the best part of Redis. A value in Redis isn’t just a blob you park under a key — it’s a data structure with its own operations and its own complexity guarantees. You don’t store a value and figure out access later; you choose the structure up front, and that choice decides which operations are cheap and which are disasters. Get the type right and the operation you need is O(1) or O(log n). Get it wrong and you’re running an O(n) command on the single thread — the exact stop-the-world footgun from Part 1.

So the skill isn’t memorising commands. It’s learning to hear the shape of the question a feature is asking, and reach for the type whose complexity makes that question cheap. Each of the five core types answers a different shape.

The five core types

Here’s the whole toolkit on one page. Read it as “what question does this type make cheap to answer?”

TypeThink of it asReach for it when
Stringa single cell — text, number, or bytesyou access the whole value at once: counters, flags, cached blobs, rate-limit tokens
Hashan object / row with named fieldsyou store many fields of one entity and want to read or update them individually
Lista linked list, fast at both endsyou push and pop at the ends: queues, stacks, capped recent-activity buffers
Setunique, unordered membershipyou care about “is X in here?” and set math: tags, likes, dedup
Sorted Seta set where every member carries a score, kept orderedyou need things ranked or ordered by a number: leaderboards, priorities, time

String — the cell

A String is one value under one key: a bit of text, a number, or raw bytes. You get and set it whole.

SET  feature:new_ui  on
GET  feature:new_ui
INCR page:home:views          # atomic counter (see Part 1)
SETEX session:42 3600 <token> # set with a 1-hour TTL

Because Redis knows a String can hold a number, it gives you INCR and INCRBY as first-class atomic operations — the free-atomicity property from Part 1. Reach for a String when the value is accessed as a whole: a counter, a boolean flag, a cached response body, a rate-limit token.

Hash — the object

A Hash is a value that itself holds a map of fields to values — an object, or a database row. The point is that you can touch one field without loading the rest.

HSET  article:99  title "Redis types"  likes 0  author 42
HGET  article:99  likes
HINCRBY article:99 likes 1     # bump one field, atomically, O(1)

That last line is the whole reason Hashes exist. To increment the like count you don’t fetch the article, parse it, edit it, and write it back — you tell the server to change one field in place. Reach for a Hash whenever you have many fields of one entity that get read and updated independently.

List — the queue

A List is an ordered sequence implemented as a linked list, which makes it fast at the ends and slow in the middle. LPUSH, RPUSH, LPOP, RPOP are all O(1).

LPUSH queue:jobs  <job>        # push onto the head, O(1)
RPOP  queue:jobs               # pop from the tail, O(1) → FIFO
LPUSH recent:42 <event>        # newest-first activity feed
LTRIM recent:42 0 99          # keep only the newest 100

That LPUSH + LTRIM pair is the canonical capped buffer: a “last 100 events” feed that never grows unbounded. Lists are the natural fit for queues, stacks, and recent-activity logs.

Set — the membership test

A Set is an unordered collection of unique members. Adding, removing, and asking “is this in the set?” are all O(1) — and on top of that you get set algebra across whole sets.

SADD    article:99:likers 42   # add a member (duplicates ignored), O(1)
SISMEMBER article:99:likers 42 # "did user 42 already like this?" O(1)
SINTER  group:a group:b        # members in BOTH sets
SUNION  tag:redis tag:cache     # everything tagged either way
SDIFF   group:a group:b        # in A but not B

Reach for a Set when uniqueness or membership is the question: tags on a post, the set of users who liked something, deduplication, or “who’s in both group A and group B?” The set operations let the server answer relationship questions your app would otherwise compute by pulling everything out and looping.

Sorted Set — the ranked index

A Sorted Set (ZSet) is the powerhouse. It’s a Set where every member also carries a floating-point score, and Redis keeps the members permanently ordered by that score. That ordering is maintained on write, so range and rank reads are O(log n) rather than requiring a sort.

ZADD leaderboard 4820 player:42   # member with a score
ZINCRBY leaderboard 10 player:42  # bump a score, stays sorted
ZREVRANGE leaderboard 0 9 WITHSCORES   # top 10, already ordered
ZRANK  leaderboard player:42      # "what rank is this member?"
ZRANGEBYSCORE events 1000 2000    # everything scored in a window

The trick is that the score can mean different things, and each meaning unlocks a use case: score = points gives you a leaderboard; score = priority gives you a priority queue; score = a timestamp gives you a time-ordered index or a sliding-window rate limiter. One structure, four classic problems solved — because in each case the ordering you need is the ordering the structure maintains for free.

The decision guide, in one line

Once you can hear the shape of the question, the choice is almost mechanical:

  • Whole value or a counter → String
  • An object whose fields update independently → Hash
  • FIFO / LIFO at the ends → List
  • Uniqueness, membership, or set math → Set
  • Ordered by a number — ranked or by time → Sorted Set

Take a real feature off a backend web dev’s desk: show the top 10 trending articles this hour, and on each article’s page show its like count. One feature, but it’s asking three different question shapes — so it wants three different types, each chosen because it makes one operation cheap.

The per-article like count is a field of the article that you bump on every like. That’s a Hash:

HINCRBY article:99 likes 1     # O(1), no need to load the whole article

The trending ranking is “these articles, ordered by likes this hour.” That’s a Sorted Set — bump the score on each like, then read the top slice already sorted:

ZINCRBY trending:articles 1 99          # on each like
ZREVRANGE trending:articles 0 9 WITHSCORES   # top 10, O(log n + 10)

No app-side sort, no ORDER BY ... LIMIT 10 hammering Postgres on every page load. Redis hands back the ten already in order.

Preventing double-likes is “has this user already liked this article?” — a membership test. That’s a Set:

SADD    article:99:likers 42
SISMEMBER article:99:likers 42   # check before counting, O(1)

Three structures, and the reason each was chosen is the same reason each time: the operation the feature needs is the operation that structure makes cheap.

The anti-pattern: a blob in a String

Here’s the most common misuse, and it’s worth spelling out because it feels reasonable right up until it bites.

The through-line of this chapter is the through-line of Redis itself: you’re not storing data and hoping, you’re choosing an execution profile. Pick the type by the operation you need most, let the structure make that operation cheap, and the single thread stays fast for everyone.

Don’t ask “how do I store this in Redis?” — ask “what operation do I need to be cheap?”, then pick the type whose complexity makes it so.