Versioning Redis Cache Keys to Prevent Stale Data During Spring Boot Deployments
Source: Dev.to
Applications often generate content that needs to be consumed later at runtime. This content may be stored in the file system, written to a database table, shared through a message broker, or placed in a shared cache. Since software systems evolve over time, the consumers of previously generated content are expected to handle these changes—but this is not always possible.
Example without versioning
@Service
public class UserProfileService {
private final UserRepository userRepository;
public UserProfileService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Cacheable("userProfile")
public UserProfile getProfile(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
return new UserProfile(
user.getId(),
user.getFirstName(),
user.getLastName()
);
}
}
Later we extend UserProfile by adding an email field:
return new UserProfile(
user.getId(),
user.getFirstName(),
user.getLastName(),
user.getEmail()
);
Because Spring had already populated the cache with instances of the previous object structure, subsequent calls to getProfile() fail with a deserialization error, e.g.:
org.springframework.data.redis.serializer.SerializationException: Cannot serialize
The exact message depends on the serializer configuration.
Why versioning is needed
Although the problem and its solution are conceptually simple, the impact can be severe in production environments. Errors may cause extended downtime, failed deployments, or forced rollbacks—especially when cache entries have long TTL values.
Versioned cache keys
A common and effective approach is to include a version identifier—optionally combined with the application name when using a shared cache—in the cache key. This ensures that newer application versions do not attempt to read incompatible cached data created by older versions.
@Configuration
public class CacheConfiguration {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(
ObjectProvider<BuildProperties> buildPropertiesProvider) {
BuildProperties buildProperties = buildPropertiesProvider.getIfAvailable();
String name = (buildProperties != null) ? buildProperties.getName() : "application";
String version = (buildProperties != null) ? buildProperties.getVersion() : "dev";
return RedisCacheConfiguration.defaultCacheConfig()
.prefixCacheNameWith(String.format("%s:%s:", name, version));
}
}
The cache key can be constructed using build information from Maven (via Maven resource filtering or the spring-boot-maven-plugin with the build-info goal), a Git commit hash (e.g., using the git-commit-id-plugin), or any other metadata injected by the CI/CD pipeline.
Resulting Redis keys
When Redis is configured with a global key prefix as shown above, cached entries are stored with versioned keys, for example:
127.0.0.1:6379> keys *
1) "payment-gateway:1.1.0:userProfile::1"
2) "payment-gateway:1.1.1:userProfile::1"
Because each application instance accesses only cache keys matching its own version, deployments and rollbacks can be performed safely without encountering deserialization errors caused by incompatible cached data.
References
- Cache Key Versioning Code Example
- Add Build Properties to a Spring Boot Application
- Injecting Git Information Into Spring
- Spring Data Redis Configuration