skip to content

How do @RedisHash entities and Redis repositories work, and how are objects stored?

level: middleimportance: should knowfreq 45%

answer

  1. @RedisHash(keyspace) + Spring Data @Id
  2. one object = one Redis hash at keyspace:id
  3. extra SET named keyspace holds all ids for findAll
  4. nested -> dotted fields, collections -> [index]
  5. uses RedisMappingContext, not template serializers

basics

~20 s

Annotate a class with @RedisHash and give it an @Id field, then declare a CrudRepository for it. Spring stores each object as a Redis hash under a key like keyspace:id, and save/findById/delete work like JPA.

solid answer

~40 s

@RedisHash("people") marks a domain class as a Redis-backed aggregate; an @Id field (org.springframework.data.annotation.Id) identifies it. Enable the feature with @EnableRedisRepositories, then declare interface PersonRepository extends CrudRepository<Person,String>. Spring Data flattens the object into a Redis hash stored under key keyspace:id (e.g. people:42) using HSET, with nested/collection properties expanded into dotted field names (address.city, or indexed like nicknames.[0]). It also maintains a SET named keyspace (people) holding all ids so findAll can enumerate them. save() upserts, findById() reads the hash and reconstructs the object, deleteById() removes it. You get TTL via @TimeToLive or @RedisHash(timeToLive=...). Note repositories use RedisMappingContext/conversions, independent of RedisTemplate serializers, and there is no query language — only derived finders backed by secondary indexes.

code

java · 20 lines
java
@RedisHash(value = "people", timeToLive = 3600)
public class Person {
    @org.springframework.data.annotation.Id
    private String id;
    private String firstname;
    private Address address;   // stored as address.city, address.zip ...
    // getters/setters
}

public interface PersonRepository extends CrudRepository<Person, String> {}

@SpringBootApplication
@EnableRedisRepositories
public class App {}

// usage
Person p = new Person();
p.setFirstname("Ann");
repo.save(p);                 // HSET people:<uuid> ... ; SADD people <uuid>
Optional<Person> found = repo.findById(p.getId());

go deeper

for a junior

Should know @RedisHash + CrudRepository gives JPA-like save/findById on Redis.

for a middle

Should describe hash flattening, the keyspace id-SET, TTL, and the correct @Id import.

for a senior

Should contrast repositories vs RedisTemplate, note whole-aggregate writes and PartialUpdate, and the mapping-context independence from serializers.

for a principal

Should weigh aggregate modeling on Redis, TTL/keyspace-notification operational needs, and when Redis repositories are the wrong tool.

**The idea** — Spring Data Redis offers a repository abstraction (like Spring Data JPA) so you can persist plain objects to Redis without writing hash commands yourself. **@RedisHash** — put on a class: `@RedisHash("people")`. The string is the **keyspace** — a prefix for all keys of this type. Each instance becomes one **Redis hash** (Redis's map-under-a-key structure). **@Id** — mark one field with `org.springframework.data.annotation.Id` (NOT the JPA `@Id`). Its value is the entity id. If you leave it null on save, Spring generates one (a UUID). The final Redis key is `keyspace:id`, e.g. `people:42`. **Enabling it** — add `@EnableRedisRepositories` (or rely on Spring Boot auto-config with the starter present) so Spring scans for `CrudRepository`/`KeyValueRepository` interfaces. **The repository** — declare an interface: ```java public interface PersonRepository extends CrudRepository<Person, String> {} ``` Spring generates the implementation. You get `save`, `findById`, `findAll`, `count`, `deleteById`, `existsById`. **How an object maps to Redis** — Spring flattens the object graph into hash fields: - Scalars: `firstname -> "Ann"`, `age -> "30"`. - Nested objects: `address.city -> "Berlin"` (dotted path). - Collections/maps: `nicknames.[0] -> "Al"`, `nicknames.[1] -> "A"`. - Bookkeeping fields like `_class` (the type) are stored too. Write uses `HSET`/`HMSET` on key `people:42`. **The keyspace SET** — because Redis has no "scan all objects of a type" concept cheaply, Spring also maintains a Redis **SET** named exactly `people` containing every id (`42`, `43`, …). `findAll()` reads that set, then fetches each hash. `save`/`delete` keep this set in sync. **TTL / expiry** — you can set time-to-live on the whole aggregate via `@RedisHash(timeToLive = 60)` (seconds) or per-instance with a `@TimeToLive` field. When a key expires, Redis deletes the hash — but see the gotcha below about the phantom-copy/index cleanup mechanism using keyspace notifications. **Independent of template serializers** — repositories use `RedisMappingContext`, `RedisConverter`, and registered `Converter`s (custom types via `RedisCustomConversions`), NOT the RedisTemplate's key/value serializers. Configuring your RedisTemplate does not affect repository storage format. **Limitations / gotchas:** - **No query language.** You only get derived query methods (`findByFirstname`) and they require secondary indexes (`@Indexed`) — a separate topic. Anything not indexed can't be queried without a full scan. - **Whole-aggregate reads/writes.** `save` rewrites the entire hash; there's no partial update through the repository (use partial updates via `RedisKeyValueTemplate`/`PartialUpdate` for that). - **@RedisHash is not JPA.** No transactions across aggregates, no joins, no lazy loading. - **Correct @Id import** — using `jakarta.persistence.Id` silently doesn't work; use the Spring Data `@Id`. - **Keyspace-notification dependency** for expiring indexed entities — index cleanup on TTL relies on Redis keyspace events being enabled. **When to use** — good for simple aggregate objects you look up by id (sessions, tokens, profiles, cache-of-record) where you want a familiar repository API. Reach for RedisTemplate directly when you need specific data structures, counters, atomic ops, or fine-grained control.

  • Why does Spring maintain an extra Redis SET named after the keyspace?
    Redis can't cheaply enumerate all keys of a logical type. The keyspace SET holds every entity id so findAll()/count() can iterate the aggregates; save and delete keep it in sync.
  • Which @Id annotation must you use on a @RedisHash entity, and what happens if you use the wrong one?
    org.springframework.data.annotation.Id. Using jakarta.persistence.Id (JPA) won't be recognized as the identifier, so key generation/mapping misbehaves.

saying these in an interview costs you the question

  • Using jakarta.persistence.@Id instead of Spring Data's @Id
  • Expecting a query language or joins on Redis repositories
  • Thinking save() does a partial field update (it rewrites the whole hash)
  • Assuming RedisTemplate serializer config governs repository storage

context