This content originally appeared on Level Up Coding - Medium and was authored by Lucas Fernandes
Recently, while developing a new feature that my team needed to implement, we came across the following problem: After our data expired and was subsequently deleted from the cache (Redis), a business flow needed to be triggered. During the initial technical refinement, the team discussed ways to implement this rule, and some ideas emerged, such as a simple CronJob or even Quartz.
However, after conducting some research, we discovered a little-known but extremely useful feature which is KeySpaceNotifications. This feature allows you to listen to key-related events such as when keys are set, deleted, or expired. These notifications enable applications to trigger real-time business logic based on Redis events. The knowledge gained from this feature motivated me to write this article for my dear Medium readers.
In this solution, we will use the following technologies: Java, Spring Boot (3.x), Redis, Docker and Gradle
Introduction
In the world of modern software development, performance and scalability are critical. Applications need to handle millions of user requests per second, deliver responses in milliseconds, and ensure minimal latency. This is where Redis, an open-source, in-memory data store, plays a pivotal role.
Redis (Remote Dictionary Server) is designed for high-performance operations, offering sub-millisecond response times. It is widely adopted across industries for use cases like caching, real-time analytics, session management, and pub/sub messaging.
Why Redis?
Redis excels in scenarios where speed is a priority, thanks to its in-memory architecture. Unlike traditional databases that rely on disk I/O for operations, Redis keeps data in RAM, significantly reducing access times. This makes it an ideal choice for caching frequently accessed data, reducing the load on primary databases, and improving overall application performance.
Redis as a Cache
Caching is a strategy used to store frequently accessed data closer to the application to reduce latency and computational overhead. Redis is particularly well-suited for caching because:
- Speed: Redis performs millions of operations per second with minimal latency
- Data Structures: It supports advanced data types like hashes, lists, sets, sorted sets, and more, making it versatile for complex caching scenarios
- TTL (Time-to-Live): Redis allows setting expiration times on keys, ensuring that cached data is automatically removed when it becomes stale
Large-Scale Adoption of Redis
Redis has become a cornerstone technology for global organizations. Companies like Twitter, Uber, Netflix, and Pinterest rely on Redis to power mission-critical systems. Its adoption is driven by:
- Scalability: Redis can handle millions of concurrent connections, making it ideal for distributed systems
- Flexibility: With features like keyspace notifications, pub/sub messaging, and Lua scripting, Redis is more than just a cache
- Open-Source and Cloud-Native: Redis is open-source and has robust managed services like AWS ElastiCache, Azure Cache for Redis, and Google Cloud Memorystore
What Are Redis Keyspace Notifications?
Keyspace Notifications in Redis allow clients to subscribe to Pub/Sub channels that broadcast events related to keys. These events can be triggered for operations such as setting a key, expiring a key, or deleting a key.
Keyspace Notifications are disabled by default and can be enabled by configuring the notify-keyspace-events parameter in Redis. Notifications are categorized into keyspace events (specific to keys) and keyevent notifications (specific to actions).
Which Events Can We To Listen
Here are some common patterns you can use to listen to Redis events:
The @* part refers to the database index, enabling flexibility across multiple databases.
Configuring Redis
Enabling Keyspace Notifications in Redis
To enable Keyspace Notifications, update the notify-keyspace-events parameter in your Redis configuration file (redis.conf) or use the CONFIG SET command. You can customize the notifications using the notify-keyspace-events configuration string. The most common options include:
- K: Keyspace events.
- E: Keyevent notifications.
- A: Alias for all events.
- $: String commands (set, append, etc.).
- g: Generic commands (del, expire, etc.).
- x: Expiry-related events.
- l: List operations.
- h: Hash operations.
- z: Sorted set operations.
Example 1: This configuration enables notifications for expired (E) and evicted (x) keys.
CONFIG SET notify-keyspace-events Ex
Example 2: To enable all notifications, use:
CONFIG SET notify-keyspace-events KEA
Deploying Redis Locally
You can use the following docker-compose file to running locally:
version: '3.9'
services:
redis:
image: redis/redis-stack:7.2.0-v4
container_name: redis
environment:
- REDIS_ARGS=--notify-keyspace-events KEA # Enable Redis Keyspace for All Notifications
ports:
- 6379:6379
- 8001:8001
Accessing via Browse
Hands-On with Java and Spring Boot
Let’s implement a Spring Boot application to listen to Redis key events and apply business logic.
Prerequisites:
- Redis installed and running
- Java 21+
- Spring Boot (Spring Data Redis)
Gradle Example:
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'br.com.ldf.medium'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'org.flywaydb:flyway-core'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
Spring Configuration for Redis
Configure your application.yml
Application.yml:
spring:
application:
name: events-from-redis
threads:
virtual:
enabled: true
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL
driverClassName: org.h2.Driver
username: sa
password:
jpa:
show-sql: true
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
cache:
type: redis
redis:
time-to-live: 60000 # 1 minute
data:
redis:
host: localhost
port: 6379
Create a configuration class to set up Redis and enable Pub/Sub.
RedisConfig
@EnableCaching
@Configuration
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public MessageListenerAdapter listenerAdapter(@Lazy MessageListener listener) {
return new MessageListenerAdapter(listener);
}
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter
) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// Add listeners for all topics
Arrays.stream(RedisPatternTopic.values())
.forEach(
topic -> container.addMessageListener(listenerAdapter, new PatternTopic(topic.getTopic()))
);
return container;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
Explanation:
The RedisConfig class is a Spring configuration class that sets up Redis-related beans and configurations for your Spring Boot application. Here’s a breakdown of its components:
Annotations:
- @EnableCaching: Enables Spring’s annotation-driven cache management capability
- @Configuration: Indicates that the class can be used by the Spring IoC container as a source of bean definitions
- @EnableRedisRepositories: Enables Redis repositories and keyspace events
Beans
- redisConnectionFactory(): Creates a LettuceConnectionFactory bean, which provides a connection to the Redis server
- listenerAdapter(MessageListener listener): Creates a MessageListenerAdapter bean, which adapts a MessageListener to the Redis message listener infrastructure
- container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter): Creates a RedisMessageListenerContainer bean, which manages Redis message listeners and their subscriptions to topics
- redisTemplate(RedisConnectionFactory connectionFactory): Creates a RedisTemplate bean, which provides high-level abstractions for Redis interactions, including serialization configurations for keys and values.
RedisPatternTopic
/**
* Enum to represent the Redis pattern topics.
*/
@Getter
public enum RedisPatternTopic {
EXPIRED_EVENT("__keyevent@*__:expired", "expired"),
SET_EVENT("__keyevent@*__:set", "set"),
DELETE_EVENT("__keyevent@*__:del", "del"),
EVICT_EVENT("__keyevent@*__:evict", "evict");
private final String topic;
private final String operation;
RedisPatternTopic(String topic, String operation) {
this.topic = topic;
this.operation = operation;
}
public static RedisPatternTopic from(String topic) {
for (RedisPatternTopic redisPatternTopic : values()) {
if (redisPatternTopic.operation.equals(topic.split(":")[1])) {
return redisPatternTopic;
}
}
throw new IllegalArgumentException("Invalid topic: " + topic);
}
}
RedisEventListener
/**
* Listener to handle expired events from Redis.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisEventListener implements MessageListener {
private final Set<HandlerRedisEventStrategy> handlers;
@Override
public void onMessage(Message message, byte[] pattern) {
var eventTopic = RedisPatternTopic.from(new String(message.getChannel()));
log.info("message:{}, event-topic={}", message, eventTopic);
handlers.stream()
.filter(handler -> handler.accept(eventTopic))
.findFirst()
.ifPresent(handler -> handler.handle(new String(message.getBody())));
}
}
Explanation:
The RedisEventListener class is a component that listens for Redis keyspace events and handles them using a set of strategies. Here’s a breakdown of its components:
- handlers: A set of HandlerRedisEventStrategy instances that define how to handle different Redis events
- onMessage(Message message, byte[] pattern): This method is called when a message is received from Redis. Converts the message channel to a RedisPatternTopic and delivery to respective handler
HandlerRedisEventStrategy:
public interface HandlerRedisEventStrategy {
boolean accept(RedisPatternTopic redisPatternTopic);
void handle(String key);
}
HandlerRedisEventStrategySet:
@Slf4j
@Component
public class HandlerRedisEventStrategySet implements HandlerRedisEventStrategy {
@Override
public boolean accept(RedisPatternTopic redisPatternTopic) {
return RedisPatternTopic.SET_EVENT.equals(redisPatternTopic);
}
@Override
public void handle(String key) {
log.info("Handling set key: {}", key);
}
}
HandlerRedisEventStrategyDel:
@Slf4j
@Component
public class HandlerRedisEventStrategyDel implements HandlerRedisEventStrategy {
@Override
public boolean accept(RedisPatternTopic redisPatternTopic) {
return RedisPatternTopic.DELETE_EVENT.equals(redisPatternTopic);
}
@Override
public void handle(String key) {
log.info("Handling expired key: {}", key);
}
}
HandlerRedisEventStrategyExpired:
@Slf4j
@Component
public class HandlerRedisEventStrategyExpired implements HandlerRedisEventStrategy {
@Override
public boolean accept(RedisPatternTopic redisPatternTopic) {
return RedisPatternTopic.EXPIRED_EVENT.equals(redisPatternTopic);
}
@Override
public void handle(String key) {
log.info("Handling expired key: {}", key);
}
}
Testing
To test the Redis pub/sub integration, we can create a simple EmployeeController for managing CRUD operations. This controller allows us to perform basic operations while demonstrating how to publish events to Redis channels.
EmployeeController:
@RestController
@RequestMapping(EmployeeController.EMPLOYEES_API_PATH)
@RequiredArgsConstructor
public class EmployeeController {
public static final String EMPLOYEES_API_PATH = "/api/employees";
private final EmployeeChangeUseCase employeeChangeUseCase;
private final EmployeeSearchUseCase employeeSearchUseCase;
private final EmployeeApplicationMapper mapper;
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Employee> getById(@PathVariable Long id) {
return ResponseEntity.ok(employeeSearchUseCase.getById(id));
}
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> create(@RequestBody @Validated EmployeeRequest request) {
var employee = employeeChangeUseCase.create(mapper.mapToModel(request));
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(employee.getId())
.toUri();
return ResponseEntity.created(location).build();
}
@PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> update(@PathVariable Long id, @RequestBody @Validated EmployeeRequest request) {
employeeChangeUseCase.update(id, mapper.mapToModel(request));
return ResponseEntity.noContent().build();
}
@DeleteMapping(value = "/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
employeeChangeUseCase.delete(id);
return ResponseEntity.noContent().build();
}
}
Inside of ours useCases, we have the cache operations:
EmployeeSearchUseCaseImpl:
@Service
@RequiredArgsConstructor
@FieldDefaults(level = lombok.AccessLevel.PRIVATE, makeFinal = true)
public class EmployeeSearchUseCaseImpl implements EmployeeSearchUseCase {
EmployeeProvider employeeProvider;
@Cacheable(cacheNames = "employeeById", key = "#id")
public Employee getById(Long id) {
return employeeProvider.getById(id);
}
}
EmployeeChangeUseCaseImpl:
@Service
@RequiredArgsConstructor
@FieldDefaults(level = lombok.AccessLevel.PRIVATE, makeFinal = true)
public class EmployeeChangeUseCaseImpl implements EmployeeChangeUseCase {
EmployeeProvider employeeProvider;
public Employee create(Employee employee) {
return employeeProvider.save(employee);
}
@CachePut(cacheNames = "employeeById", key = "#id")
public Employee update(Long id, Employee employee) {
return employeeProvider.update(id, employee);
}
@CacheEvict(cacheNames = "employeeById", key = "#id")
public void delete(Long id) {
employeeProvider.delete(id);
}
}
Running
Lets simulate the following operations:
- Creating new employee
- Searching by Id
- Deleting by Id
- Creating new employee
- Searching by Id
- Redis Expired Event deleting key in cache
project reference: https://github.com/Medium-Artigos/events-from-redis
Conclusion
Redis is much more than a simple key-value store; its rich feature set enables developers to build highly responsive, scalable, and efficient applications. Among these features, Keyspace Notifications stand out as a powerful mechanism for real-time event-driven architectures. By leveraging these notifications, applications can react to changes in Redis keys — such as expirations, deletions, and updates — and trigger custom business logic instantly.
In this article, we explored:
- The fundamentals of Redis Keyspace Notifications and their practical use cases.
- How to configure Redis to enable these notifications.
- A hands-on example of integrating Redis Keyspace Notifications into a Spring Boot application.
Keyspace Notifications are particularly useful in scenarios like cache invalidation, event-driven workflows, and monitoring system changes. However, it’s essential to design your application carefully to avoid unnecessary load on your Redis server, especially when working with large-scale data or high-frequency events.
Feel free to leave comments or suggestions, and share with other developers who might benefit from this solution!
Follow me for more exciting content
➡️ Medium
➡️ Substack
Listening to Events From Redis in Your Spring Boot Application was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Lucas Fernandes
Lucas Fernandes | Sciencx (2025-01-17T19:02:12+00:00) Listening to Events From Redis in Your Spring Boot Application. Retrieved from https://www.scien.cx/2025/01/17/listening-to-events-from-redis-in-your-spring-boot-application/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.