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): returnsnullif 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 toHSET.puta single field: updates only theemailfield; 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-1means 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");
}
}
@Containerspins up a real Redis instance in Docker for each test class.@DynamicPropertySourceoverrides host/port so Spring Boot connects to the right container.containsExactlyverifies 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;LRANGEcommands slow down over time. - Forgetting to set TTL: Redis doesn’t automatically delete keys by default; garbage data accumulates forever. Always consider
expirefor 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.