0
# Dependency Injection and Named Caches
1
2
Support for multiple cache instances using named cache injection with Play's dependency injection framework. This allows applications to use different cache configurations for different purposes (e.g., session cache, data cache, temporary cache).
3
4
## Capabilities
5
6
### @NamedCache Annotation (Java)
7
8
The @NamedCache annotation is used to inject specific named cache instances.
9
10
```java { .api }
11
/**
12
* Qualifier annotation for named cache injection
13
*/
14
@Qualifier
15
@Retention(RetentionPolicy.RUNTIME)
16
public @interface NamedCache {
17
/**
18
* The name of the cache to inject
19
* @return cache name
20
*/
21
String value();
22
}
23
```
24
25
**Usage Examples:**
26
27
```java
28
import play.cache.AsyncCacheApi;
29
import play.cache.SyncCacheApi;
30
import play.cache.NamedCache;
31
import javax.inject.Inject;
32
import java.util.concurrent.CompletionStage;
33
34
public class UserService {
35
private final AsyncCacheApi sessionCache;
36
private final AsyncCacheApi dataCache;
37
private final SyncCacheApi tempCache;
38
39
@Inject
40
public UserService(
41
@NamedCache("session") AsyncCacheApi sessionCache,
42
@NamedCache("data") AsyncCacheApi dataCache,
43
@NamedCache("temp") SyncCacheApi tempCache
44
) {
45
this.sessionCache = sessionCache;
46
this.dataCache = dataCache;
47
this.tempCache = tempCache;
48
}
49
50
// Use session cache for user sessions (short-lived)
51
public CompletionStage<Done> cacheUserSession(String sessionId, UserSession session) {
52
return sessionCache.set("session:" + sessionId, session, 3600); // 1 hour
53
}
54
55
// Use data cache for user data (longer-lived)
56
public CompletionStage<Done> cacheUserData(String userId, User user) {
57
return dataCache.set("user:" + userId, user, 86400); // 24 hours
58
}
59
60
// Use temp cache for temporary calculations (synchronous)
61
public void cacheTempResult(String key, Object result) {
62
tempCache.set("temp:" + key, result, 300); // 5 minutes
63
}
64
}
65
```
66
67
### NamedCacheImpl Class (Java)
68
69
Implementation class for the @NamedCache annotation, used internally by the DI framework.
70
71
```java { .api }
72
/**
73
* Implementation of NamedCache annotation for dependency injection
74
* See https://issues.scala-lang.org/browse/SI-8778 for why this is implemented in Java
75
*/
76
public class NamedCacheImpl implements NamedCache, Serializable {
77
/**
78
* Constructor for creating NamedCache instances
79
* @param value the cache name
80
*/
81
public NamedCacheImpl(String value);
82
83
/**
84
* Get the cache name
85
* @return the cache name
86
*/
87
public String value();
88
89
/**
90
* Hash code implementation following java.lang.Annotation specification
91
* @return hash code
92
*/
93
public int hashCode();
94
95
/**
96
* Equals implementation for annotation comparison
97
* @param o object to compare
98
* @return true if equal
99
*/
100
public boolean equals(Object o);
101
102
/**
103
* String representation of the annotation
104
* @return string representation
105
*/
106
public String toString();
107
108
/**
109
* Returns the annotation type
110
* @return NamedCache.class
111
*/
112
public Class<? extends Annotation> annotationType();
113
}
114
```
115
116
### Scala Type Alias
117
118
Scala code can reference the Java annotation through a type alias.
119
120
```scala { .api }
121
// Type alias in play.api.cache package object
122
package play.api
123
124
/**
125
* Contains the Cache access API
126
*/
127
package object cache {
128
type NamedCache = play.cache.NamedCache
129
}
130
```
131
132
**Usage Examples:**
133
134
```scala
135
import play.api.cache.{AsyncCacheApi, NamedCache}
136
import javax.inject.Inject
137
138
class ProductService @Inject()(
139
@NamedCache("products") productCache: AsyncCacheApi,
140
@NamedCache("categories") categoryCache: AsyncCacheApi,
141
defaultCache: AsyncCacheApi // Default unnamed cache
142
) {
143
144
import scala.concurrent.duration._
145
import scala.concurrent.ExecutionContext
146
147
// Use product-specific cache
148
def cacheProduct(product: Product)(implicit ec: ExecutionContext) = {
149
productCache.set(s"product:${product.id}", product, 2.hours)
150
}
151
152
// Use category-specific cache
153
def cacheCategory(category: Category)(implicit ec: ExecutionContext) = {
154
categoryCache.set(s"category:${category.id}", category, 6.hours)
155
}
156
157
// Use default cache for temporary data
158
def cacheTempData(key: String, data: Any)(implicit ec: ExecutionContext) = {
159
defaultCache.set(s"temp:$key", data, 30.minutes)
160
}
161
}
162
```
163
164
## Configuration Examples
165
166
### Application Configuration
167
168
Configure multiple named caches in `application.conf`:
169
170
```hocon
171
# Default cache configuration
172
play.cache.defaultCache = "default"
173
play.cache.bindCaches = ["session", "data", "temp"]
174
175
# EhCache configuration for multiple caches
176
play.cache.createBoundCaches = true
177
178
# Cache-specific configurations
179
ehcache {
180
caches {
181
default {
182
maxEntriesLocalHeap = 1000
183
timeToLiveSeconds = 3600
184
}
185
session {
186
maxEntriesLocalHeap = 5000
187
timeToLiveSeconds = 1800 # 30 minutes
188
}
189
data {
190
maxEntriesLocalHeap = 10000
191
timeToLiveSeconds = 86400 # 24 hours
192
}
193
temp {
194
maxEntriesLocalHeap = 500
195
timeToLiveSeconds = 300 # 5 minutes
196
}
197
}
198
}
199
```
200
201
### Guice Module Configuration
202
203
Custom Guice module for advanced cache binding:
204
205
```java
206
import com.google.inject.AbstractModule;
207
import com.google.inject.name.Names;
208
import play.cache.AsyncCacheApi;
209
import play.cache.NamedCacheImpl;
210
211
public class CacheModule extends AbstractModule {
212
@Override
213
protected void configure() {
214
// Bind named caches
215
bind(AsyncCacheApi.class)
216
.annotatedWith(new NamedCacheImpl("redis"))
217
.to(RedisAsyncCacheApi.class);
218
219
bind(AsyncCacheApi.class)
220
.annotatedWith(new NamedCacheImpl("memory"))
221
.to(MemoryAsyncCacheApi.class);
222
223
bind(AsyncCacheApi.class)
224
.annotatedWith(new NamedCacheImpl("distributed"))
225
.to(HazelcastAsyncCacheApi.class);
226
}
227
}
228
```
229
230
## Multi-Cache Patterns
231
232
### Cache Hierarchies
233
234
Use multiple caches in a hierarchy for different performance characteristics:
235
236
```java
237
public class DataService {
238
private final SyncCacheApi l1Cache; // Fast in-memory cache
239
private final AsyncCacheApi l2Cache; // Slower but larger cache
240
private final AsyncCacheApi backupCache; // Distributed/persistent cache
241
242
@Inject
243
public DataService(
244
@NamedCache("l1") SyncCacheApi l1Cache,
245
@NamedCache("l2") AsyncCacheApi l2Cache,
246
@NamedCache("backup") AsyncCacheApi backupCache
247
) {
248
this.l1Cache = l1Cache;
249
this.l2Cache = l2Cache;
250
this.backupCache = backupCache;
251
}
252
253
public CompletionStage<Optional<Data>> getData(String key) {
254
// Try L1 cache first (synchronous, fast)
255
Optional<Data> l1Result = l1Cache.getOptional(key);
256
if (l1Result.isPresent()) {
257
return CompletableFuture.completedFuture(l1Result);
258
}
259
260
// Try L2 cache (asynchronous, larger)
261
return l2Cache.getOptional(key)
262
.thenCompose(l2Result -> {
263
if (l2Result.isPresent()) {
264
// Store in L1 for next time
265
l1Cache.set(key, l2Result.get(), 300);
266
return CompletableFuture.completedFuture(l2Result);
267
}
268
269
// Try backup cache
270
return backupCache.getOptional(key)
271
.thenApply(backupResult -> {
272
if (backupResult.isPresent()) {
273
// Store in both L1 and L2
274
l1Cache.set(key, backupResult.get(), 300);
275
l2Cache.set(key, backupResult.get(), 3600);
276
}
277
return backupResult;
278
});
279
});
280
}
281
}
282
```
283
284
### Cache Specialization
285
286
Use different caches for different types of data:
287
288
```scala
289
import play.api.cache.{AsyncCacheApi, NamedCache}
290
import javax.inject.Inject
291
import scala.concurrent.duration._
292
293
class CacheService @Inject()(
294
@NamedCache("user-sessions") sessionCache: AsyncCacheApi,
295
@NamedCache("api-responses") apiCache: AsyncCacheApi,
296
@NamedCache("static-content") staticCache: AsyncCacheApi,
297
@NamedCache("analytics") analyticsCache: AsyncCacheApi
298
) {
299
300
// Short-lived session data
301
def cacheUserSession(sessionId: String, session: UserSession) = {
302
sessionCache.set(s"session:$sessionId", session, 30.minutes)
303
}
304
305
// Medium-lived API responses
306
def cacheApiResponse(endpoint: String, response: ApiResponse) = {
307
apiCache.set(s"api:$endpoint", response, 2.hours)
308
}
309
310
// Long-lived static content
311
def cacheStaticContent(path: String, content: StaticContent) = {
312
staticCache.set(s"static:$path", content, 24.hours)
313
}
314
315
// Analytics data with custom expiration
316
def cacheAnalytics(key: String, data: AnalyticsData) = {
317
analyticsCache.set(s"analytics:$key", data, Duration.Inf) // No expiration
318
}
319
}
320
```
321
322
### Cache Invalidation Coordination
323
324
Coordinate invalidation across multiple named caches:
325
326
```java
327
public class CacheManager {
328
private final AsyncCacheApi userCache;
329
private final AsyncCacheApi sessionCache;
330
private final AsyncCacheApi dataCache;
331
332
@Inject
333
public CacheManager(
334
@NamedCache("users") AsyncCacheApi userCache,
335
@NamedCache("sessions") AsyncCacheApi sessionCache,
336
@NamedCache("data") AsyncCacheApi dataCache
337
) {
338
this.userCache = userCache;
339
this.sessionCache = sessionCache;
340
this.dataCache = dataCache;
341
}
342
343
// Invalidate all data related to a user
344
public CompletionStage<Done> invalidateUser(String userId) {
345
CompletionStage<Done> userEviction = userCache.remove("user:" + userId);
346
CompletionStage<Done> sessionEviction = sessionCache.remove("session:" + userId);
347
CompletionStage<Done> dataEviction = dataCache.remove("userdata:" + userId);
348
349
return CompletableFuture.allOf(
350
userEviction.toCompletableFuture(),
351
sessionEviction.toCompletableFuture(),
352
dataEviction.toCompletableFuture()
353
).thenApply(v -> Done.getInstance());
354
}
355
356
// Clear all caches (admin operation)
357
public CompletionStage<Done> clearAllCaches() {
358
return CompletableFuture.allOf(
359
userCache.removeAll().toCompletableFuture(),
360
sessionCache.removeAll().toCompletableFuture(),
361
dataCache.removeAll().toCompletableFuture()
362
).thenApply(v -> Done.getInstance())
363
.exceptionally(throwable -> {
364
// Some caches might not support removeAll()
365
if (throwable.getCause() instanceof UnsupportedOperationException) {
366
// Log warning and continue
367
return Done.getInstance();
368
}
369
throw new RuntimeException(throwable);
370
});
371
}
372
}
373
```
374
375
## Testing with Named Caches
376
377
### Test Configuration
378
379
```java
380
public class CacheTestModule extends AbstractModule {
381
@Override
382
protected void configure() {
383
// Use in-memory caches for testing
384
bind(AsyncCacheApi.class)
385
.annotatedWith(new NamedCacheImpl("test-cache"))
386
.to(DefaultAsyncCacheApi.class);
387
}
388
}
389
```
390
391
### Test Examples
392
393
```java
394
public class UserServiceTest {
395
@Inject
396
@NamedCache("test-cache")
397
private AsyncCacheApi testCache;
398
399
@Inject
400
private UserService userService;
401
402
@Test
403
public void testCacheOperations() {
404
// Test with named cache
405
User user = new User("123", "John Doe");
406
407
CompletionStage<Done> setResult = testCache.set("user:123", user, 3600);
408
CompletionStage<Optional<User>> getResult = testCache.getOptional("user:123");
409
410
// Verify cache operations
411
assertThat(setResult.toCompletableFuture().join()).isEqualTo(Done.getInstance());
412
assertThat(getResult.toCompletableFuture().join()).contains(user);
413
}
414
}
415
```
416
417
## Best Practices
418
419
### Cache Naming Conventions
420
421
```java
422
// Use descriptive, hierarchical names
423
@NamedCache("user-sessions") // User session data
424
@NamedCache("product-catalog") // Product information
425
@NamedCache("search-results") // Search result caching
426
@NamedCache("api-rate-limits") // Rate limiting data
427
@NamedCache("temp-calculations") // Temporary computation results
428
```
429
430
### Configuration Management
431
432
```scala
433
// Use configuration-driven cache selection
434
class ConfigurableCacheService @Inject()(
435
@NamedCache("primary") primaryCache: AsyncCacheApi,
436
@NamedCache("fallback") fallbackCache: AsyncCacheApi,
437
config: Configuration
438
) {
439
440
private val useFailover = config.get[Boolean]("cache.enable-failover")
441
442
def getCache: AsyncCacheApi = {
443
if (useFailover) fallbackCache else primaryCache
444
}
445
}
446
```
447
448
### Error Handling
449
450
```java
451
public class RobustCacheService {
452
private final AsyncCacheApi primaryCache;
453
private final AsyncCacheApi fallbackCache;
454
455
@Inject
456
public RobustCacheService(
457
@NamedCache("primary") AsyncCacheApi primaryCache,
458
@NamedCache("fallback") AsyncCacheApi fallbackCache
459
) {
460
this.primaryCache = primaryCache;
461
this.fallbackCache = fallbackCache;
462
}
463
464
public CompletionStage<Optional<Data>> getWithFallback(String key) {
465
return primaryCache.getOptional(key)
466
.exceptionally(throwable -> {
467
// Log primary cache failure
468
logger.warn("Primary cache failed, trying fallback", throwable);
469
return Optional.<Data>empty();
470
})
471
.thenCompose(result -> {
472
if (result.isPresent()) {
473
return CompletableFuture.completedFuture(result);
474
}
475
// Try fallback cache
476
return fallbackCache.getOptional(key);
477
});
478
}
479
}
480
```