Skip to content

Commit 2f56ad8

Browse files
pditommasoclaude
andauthored
Add micronaut-cache-redis module (#49)
* Add micronaut-redis module Micronaut cache implementation using Jedis driver, providing drop-in compatibility with the official Micronaut Redis (Lettuce) cache module. Features: - Works with @Cacheable, @cACHEpUT, @CacheInvalidate annotations - Uses Jedis driver with JedisPool for connection pooling - Same configuration namespace as official Lettuce implementation - Supports expire-after-write and expire-after-access policies - Custom expiration policy support - Async cache operations via AsyncCache 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Rename micronaut-redis to micronaut-cache-redis - Rename module directory and update all references - Add trace logging to RedisCache for debugging key operations - Add changelog.txt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 82c66ab commit 2f56ad8

File tree

15 files changed

+1380
-0
lines changed

15 files changed

+1380
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ jobs:
8484
bash publish.sh lib-serde-moshi
8585
bash publish.sh lib-serde-jackson
8686
bash publish.sh lib-trace
87+
bash publish.sh micronaut-cache-redis
8788
8889
env:
8990
GRADLE_OPTS: '-Dorg.gradle.daemon=false'

.github/workflows/generate-submit-dependencies.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
"lib-serde-jackson",
3636
"lib-trace",
3737
"lib-util-http",
38+
"micronaut-cache-redis",
3839
"wave-api",
3940
"wave-utils"
4041
]

micronaut-cache-redis/README.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Micronaut Redis Cache
2+
3+
A Micronaut cache implementation using the Jedis Redis driver, providing drop-in compatibility with the official Micronaut Redis (Lettuce) cache module.
4+
5+
## Features
6+
7+
- Works with Micronaut's `@Cacheable`, `@CachePut`, `@CacheInvalidate` annotations
8+
- Uses Jedis driver with JedisPool for connection pooling
9+
- Same configuration namespace as the official Lettuce implementation
10+
- Supports expiration policies (expire-after-write, expire-after-access)
11+
- Custom expiration policy support
12+
- Async cache operations via `AsyncCache`
13+
14+
## Installation
15+
16+
Add the dependency to your `build.gradle`:
17+
18+
```groovy
19+
dependencies {
20+
implementation 'io.seqera:micronaut-cache-redis:1.0.0'
21+
}
22+
```
23+
24+
## Configuration
25+
26+
The module uses the same configuration as the official Micronaut Redis cache:
27+
28+
```yaml
29+
redis:
30+
caches:
31+
my-cache:
32+
expire-after-write: 1h
33+
another-cache:
34+
expire-after-access: 30m
35+
invalidate-scan-count: 100
36+
```
37+
38+
### Configuration Options
39+
40+
| Property | Type | Description |
41+
|----------|------|-------------|
42+
| `expire-after-write` | Duration | TTL after writing a value |
43+
| `expire-after-access` | Duration | TTL after accessing a value (touch-based) |
44+
| `expiration-after-write-policy` | String | Custom policy class name |
45+
| `invalidate-scan-count` | Long | SCAN batch size for invalidateAll (default: 100) |
46+
| `key-serializer` | Class | Custom key serializer |
47+
| `value-serializer` | Class | Custom value serializer |
48+
| `charset` | Charset | Character encoding for keys |
49+
50+
### Default Configuration
51+
52+
You can set defaults for all caches:
53+
54+
```yaml
55+
redis:
56+
cache:
57+
expire-after-write: 2h
58+
charset: UTF-8
59+
caches:
60+
my-cache:
61+
# inherits defaults, can override
62+
expire-after-write: 30m
63+
```
64+
65+
## Usage
66+
67+
### Provide a JedisPool Bean
68+
69+
The module requires a `JedisPool` bean to be provided by your application:
70+
71+
```java
72+
@Factory
73+
public class RedisFactory {
74+
75+
@Singleton
76+
public JedisPool jedisPool() {
77+
return new JedisPool("localhost", 6379);
78+
}
79+
}
80+
```
81+
82+
### Using with @Cacheable
83+
84+
```java
85+
@Singleton
86+
public class UserService {
87+
88+
@Cacheable("users")
89+
public User findById(Long id) {
90+
// This will be cached
91+
return userRepository.findById(id);
92+
}
93+
94+
@CachePut("users")
95+
public User update(Long id, User user) {
96+
return userRepository.save(user);
97+
}
98+
99+
@CacheInvalidate("users")
100+
public void delete(Long id) {
101+
userRepository.deleteById(id);
102+
}
103+
}
104+
```
105+
106+
### Programmatic Access
107+
108+
```java
109+
@Singleton
110+
public class CacheService {
111+
112+
private final SyncCache<JedisPool> cache;
113+
114+
public CacheService(@Named("my-cache") SyncCache<JedisPool> cache) {
115+
this.cache = cache;
116+
}
117+
118+
public void example() {
119+
// Put
120+
cache.put("key", "value");
121+
122+
// Get
123+
Optional<String> value = cache.get("key", String.class);
124+
125+
// Get with supplier
126+
String result = cache.get("key", String.class, () -> "default");
127+
128+
// Put if absent
129+
Optional<String> existing = cache.putIfAbsent("key", "value");
130+
131+
// Invalidate
132+
cache.invalidate("key");
133+
cache.invalidateAll();
134+
135+
// Async operations
136+
cache.async().get("key", String.class)
137+
.thenAccept(opt -> opt.ifPresent(System.out::println));
138+
}
139+
}
140+
```
141+
142+
### Custom Value Serializer
143+
144+
The default serializer uses JDK serialization, which requires cached objects to implement `Serializable`. For objects that don't implement `Serializable` (e.g., generated DTOs), you can use a JSON-based serializer.
145+
146+
Create a Jackson serializer:
147+
148+
```java
149+
@Singleton
150+
public class JacksonObjectSerializer implements ObjectSerializer {
151+
152+
@Inject
153+
private ObjectMapper objectMapper;
154+
155+
@Override
156+
public Optional<byte[]> serialize(Object object) {
157+
if (object == null) {
158+
return Optional.empty();
159+
}
160+
try {
161+
return Optional.of(objectMapper.writeValueAsBytes(object));
162+
} catch (IOException e) {
163+
throw new RuntimeException("Failed to serialize object", e);
164+
}
165+
}
166+
167+
@Override
168+
public <T> Optional<T> deserialize(byte[] bytes, Class<T> requiredType) {
169+
if (bytes == null || bytes.length == 0) {
170+
return Optional.empty();
171+
}
172+
try {
173+
return Optional.ofNullable(objectMapper.readValue(bytes, requiredType));
174+
} catch (IOException e) {
175+
throw new RuntimeException("Failed to deserialize object", e);
176+
}
177+
}
178+
179+
@Override
180+
public <T> Optional<T> deserialize(byte[] bytes, Argument<T> requiredType) {
181+
if (bytes == null || bytes.length == 0) {
182+
return Optional.empty();
183+
}
184+
try {
185+
return Optional.ofNullable(objectMapper.readValue(bytes,
186+
objectMapper.constructType(requiredType.asType())));
187+
} catch (IOException e) {
188+
throw new RuntimeException("Failed to deserialize object", e);
189+
}
190+
}
191+
}
192+
```
193+
194+
Configure it:
195+
196+
```yaml
197+
redis:
198+
caches:
199+
my-cache:
200+
value-serializer: com.example.JacksonObjectSerializer
201+
```
202+
203+
### Custom Expiration Policy
204+
205+
Implement `ExpirationAfterWritePolicy` for dynamic TTL:
206+
207+
```java
208+
@Singleton
209+
public class TypeBasedExpirationPolicy implements ExpirationAfterWritePolicy {
210+
211+
@Override
212+
public long getExpirationAfterWrite(Object value) {
213+
if (value instanceof TemporaryData) {
214+
return Duration.ofMinutes(5).toMillis();
215+
}
216+
return Duration.ofHours(1).toMillis();
217+
}
218+
}
219+
```
220+
221+
Configure it:
222+
223+
```yaml
224+
redis:
225+
caches:
226+
my-cache:
227+
expiration-after-write-policy: com.example.TypeBasedExpirationPolicy
228+
```
229+
230+
## Migration from Lettuce
231+
232+
This module is designed as a drop-in replacement for `micronaut-redis-lettuce` cache. To migrate:
233+
234+
1. Replace the dependency
235+
2. Provide a `JedisPool` bean instead of Lettuce connection
236+
3. Configuration remains the same
237+
238+
## License
239+
240+
Apache License 2.0

micronaut-cache-redis/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0

micronaut-cache-redis/build.gradle

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2026, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
plugins {
19+
id 'io.seqera.java-library-conventions'
20+
id 'io.seqera.groovy-library-conventions'
21+
// Micronaut minimal lib
22+
// https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/
23+
id "io.micronaut.minimal.library" version '4.5.3'
24+
}
25+
26+
group = 'io.seqera'
27+
version = "${project.file('VERSION').text.trim()}"
28+
29+
dependencies {
30+
// Micronaut cache core
31+
api "io.micronaut.cache:micronaut-cache-core:4.3.1"
32+
33+
// Jedis Redis client
34+
api 'redis.clients:jedis:5.1.5'
35+
36+
// Micronaut runtime
37+
implementation "io.micronaut:micronaut-runtime:${micronautCoreVersion}"
38+
implementation "io.micronaut:micronaut-context:${micronautCoreVersion}"
39+
implementation "io.micronaut:micronaut-core:${micronautCoreVersion}"
40+
41+
// JSpecify annotations
42+
compileOnly 'org.jspecify:jspecify:1.0.0'
43+
44+
// Groovy support for compilation
45+
compileOnly "io.micronaut:micronaut-inject-groovy:${micronautCoreVersion}"
46+
47+
// Test dependencies
48+
testImplementation testFixtures(project(':lib-fixtures-redis'))
49+
testImplementation "io.micronaut:micronaut-inject-groovy:${micronautCoreVersion}"
50+
testImplementation "io.micronaut.test:micronaut-test-spock:${micronautTestVersion}"
51+
testImplementation 'org.testcontainers:testcontainers:1.20.6'
52+
}
53+
54+
test {
55+
useJUnitPlatform()
56+
}
57+
58+
micronaut {
59+
version '4.8.3'
60+
runtime("netty")
61+
processing {
62+
incremental(true)
63+
}
64+
importMicronautPlatform = false
65+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# micronaut-cache-redis changelog
2+
3+
1.0.0 - 8 Jan 2026
4+
- Initial release of micronaut-cache-redis
5+
- Micronaut cache implementation using Jedis Redis driver
6+
- Drop-in compatibility with official Micronaut Redis (Lettuce) cache module
7+
- Support for @Cacheable, @CachePut, @CacheInvalidate annotations
8+
- JedisPool-based connection pooling
9+
- Expiration policies: expire-after-write, expire-after-access
10+
- Custom ExpirationAfterWritePolicy support for dynamic TTL
11+
- AsyncCache support via CompletableFuture
12+
- Configurable key and value serializers
13+
- Same configuration namespace as official Lettuce implementation

0 commit comments

Comments
 (0)