Redis Basics: Data Types and When to Use Them

1. Overview

Redis is an in-memory data store, often misunderstood as merely a cache. In reality, Redis provides several different data types (data structures), each suited to a particular set of problems.

This article helps you master Redis’s 5 most common data types, how to use them with Spring Data Redis, and more importantly: when to choose which type. After reading, you’ll know how to avoid the classic mistake of “using String for everything” and then making your life harder when updating data.

2. Setting up the environment

Add the Spring Data Redis dependency to your project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Declare the connection in application.yml:

spring:
  data:
    redis:
      host: localhost
      port: 6379

Run Redis with Docker for quick setup:

docker run -d --name redis-dev -p 6379:6379 redis:7-alpine

The examples below use RedisTemplate<String, String>, which Spring Boot auto-configures; you don’t need to manually declare a bean.

3. String: The Simplest Type

String is the basic type, storing a single value paired with a key. The value can be text, a number, or serialized JSON.

@Service
public class SessionTokenService {

    private final RedisTemplate<String, String> redisTemplate;

    public SessionTokenService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveToken(String userId, String token) {
        redisTemplate.opsForValue()
            .set("session:" + userId, token, Duration.ofMinutes(30));
    }

    public String getToken(String userId) {
        return redisTemplate.opsForValue().get("session:" + userId);
    }
}
  • set(key, value, ttl): saves the token with a 30-minute TTL; Redis automatically removes it when expired.
  • get(key): returns null if the key doesn’t exist or has expired.

Output when calling getToken with an expired token:

null

When to use: session tokens, caching a pre-serialized object (JSON), simple counters (INCR), feature flags. String also supports atomic increment:

Long visits = redisTemplate.opsForValue().increment("page:views:home");

Note: don’t store a large object and serialize/deserialize the whole thing just to update one field — that’s a sign you should switch to Hash.

4. Hash: Objects with Multiple Fields

Hash is like a Map<String, String> inside a single key, allowing you to update individual fields without reading or writing the entire object.

@Service
public class UserProfileCache {

    private final RedisTemplate<String, String> redisTemplate;

    public UserProfileCache(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveProfile(String userId, Map<String, String> fields) {
        redisTemplate.opsForHash().putAll("user:" + userId, fields);
    }

    public void updateEmail(String userId, String newEmail) {
        redisTemplate.opsForHash().put("user:" + userId, "email", newEmail);
    }

    public Map<Object, Object> getProfile(String userId) {
        return redisTemplate.opsForHash().entries("user:" + userId);
    }
}
  • putAll: write multiple fields at once, similar to HSET.
  • put a single field: updates only the email field; other fields (name, age…) are unaffected.
  • entries: retrieve all field-value pairs of the hash.

Compared to String: if you use String + JSON, each time you change an email you must GET the entire JSON, parse it, modify it, serialize it again, then SET it. With Hash, it’s just one HSET command.

When to use: objects with multiple fields that update independently — user profiles, product details, per-tenant configuration.

5. List: Queues and Ordered History

List is an ordered collection that supports push/pop at both ends. Suitable for queues or storing the N most recent items.

@Service
public class RecentActivityService {

    private final RedisTemplate<String, String> redisTemplate;

    public RecentActivityService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void logActivity(String userId, String activity) {
        String key = "activity:" + userId;
        redisTemplate.opsForList().leftPush(key, activity);
        redisTemplate.opsForList().trim(key, 0, 9); // keep only the 10 most recent items
    }

    public List<String> getRecentActivities(String userId) {
        return redisTemplate.opsForList().range("activity:" + userId, 0, -1);
    }
}
  • leftPush: add new activity to the front of the list.
  • trim(key, 0, 9): trim the list to keep only the first 10 elements, preventing unbounded growth.
  • range(0, -1): retrieve all elements; index -1 means the last element.

When to use: timelines of recent activity, simple job queues (combined with BLPOP for waiting consumers), undo stacks.

Note: List isn’t suitable if you need to search by value or perform random access frequently — index access has O(n) complexity for long lists.

6. Set: Unique, Unordered Collections

Set stores unique values and supports set operations (union, intersection, difference) very efficiently.

@Service
public class TagService {

    private final RedisTemplate<String, String> redisTemplate;

    public TagService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void addTags(String articleId, String... tags) {
        redisTemplate.opsForSet().add("article:tags:" + articleId, tags);
    }

    public boolean hasTag(String articleId, String tag) {
        return redisTemplate.opsForSet()
            .isMember("article:tags:" + articleId, tag);
    }

    public Set<String> commonTags(String articleId1, String articleId2) {
        return redisTemplate.opsForSet()
            .intersect("article:tags:" + articleId1, "article:tags:" + articleId2);
    }
}
  • add: add multiple tags; Redis automatically removes duplicates.
  • isMember: check existence in O(1) time, much faster than iterating a List.
  • intersect: find common tags between two articles — useful for recommending related articles.

When to use: tags/categories, unique daily visitor lists (SADD visitors:2026-10-05 userId), fast membership checks (e.g., has the user signed up for this event?).

7. Sorted Set: Ranking by Score

Sorted Set (ZSet) is like Set, but each member has a score for sorting. It’s the strongest type for leaderboard problems.

@Service
public class LeaderboardService {

    private final RedisTemplate<String, String> redisTemplate;

    public LeaderboardService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void addScore(String playerId, double score) {
        redisTemplate.opsForZSet().add("leaderboard:weekly", playerId, score);
    }

    public Long getRank(String playerId) {
        // rank 0-based, null if not found
        return redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", playerId);
    }

    public Set<String> getTop10() {
        return redisTemplate.opsForZSet()
            .reverseRange("leaderboard:weekly", 0, 9);
    }
}
  • add(key, member, score): add or update a player’s score.
  • reverseRank: get rank in descending order by score (highest rank = highest score).
  • reverseRange(0, 9): get top 10; Redis pre-sorts, no need to sort in application.

Output example when calling getTop10():

["playerA", "playerC", "playerB"]

When to use: leaderboards, priority queues (score = priority), rate limiting by time window (score = timestamp).

8. Testing with JUnit 5 and Testcontainers

To test Redis-related logic without affecting real data, use Testcontainers.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class LeaderboardServiceTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void redisProps(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private LeaderboardService leaderboardService;

    @Test
    void shouldReturnTopPlayersInOrder() {
        leaderboardService.addScore("playerA", 100);
        leaderboardService.addScore("playerB", 50);
        leaderboardService.addScore("playerC", 80);

        Set<String> top = leaderboardService.getTop10();

        assertThat(top).containsExactly("playerA", "playerC", "playerB");
    }
}
  • @Container spins up a real Redis instance in Docker for each test class.
  • @DynamicPropertySource overrides host/port so Spring Boot connects to the right container.
  • containsExactly verifies exact order — Sorted Set guarantees results are always sorted by descending score.

9. Quick Comparison Table

Type Structure Use When
String key → value caching objects, counters, session tokens
Hash key → field-value map objects with multiple fields, updating individual fields
List key → ordered list queues, recent items, timelines
Set key → unique collection tags, membership checks, set operations
Sorted Set key → members with scores leaderboards, rate limiting, priority queues

10. Common Mistakes

  • Using String for data that updates partially: leads to race conditions when multiple threads read-modify-write JSON. Switch to Hash for atomic field updates.
  • Not trimming Lists: lists grow unbounded, consuming RAM; LRANGE commands slow down over time.
  • Forgetting to set TTL: Redis doesn’t automatically delete keys by default; garbage data accumulates forever. Always consider expire for temporary data like sessions and caches.
  • Using Set for data that needs order: Set doesn’t guarantee iteration order; use Sorted Set if you need sorting.

Note: Redis is in-memory — all data lives in RAM. Don’t store large, long-lived, or data requiring complex ACID transactions (e.g., account balances) in Redis as your only source of truth.

11. Conclusion

Redis isn’t just “a big cache” — it’s a flexible data structure toolkit. Choosing the right data type — String for single values, Hash for multi-field objects, List for queues, Set for membership, Sorted Set for rankings — results in cleaner code, fewer commands, and leverages Redis’s atomic operations instead of handling that logic in your application layer.

rediscachingspring-bootnosqlperformance