ben-manes/caffeine · error · IllegalStateException
An invalid state was detected, occurring when the key's equa
Error message
An invalid state was detected, occurring when the key's equals or hashCode was modified while residing in the cache. This violation of the Map contract can lead to non-deterministic behavior (key: %s, key type: %s, node type: %s, cache type: %s).
What it means
BoundedLocalCache.requireIsAlive throws IllegalStateException when a node retrieved from the internal hash table no longer matches its bucket, which happens when a key's equals() or hashCode() was mutated after insertion. The cache relies on the Map contract that keys are immutable while resident; a broken key makes lookups non-deterministic, so Caffeine fails fast with a diagnostic message identifying the key, key type, node type, and cache type.
Source
Thrown at caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java:297
evictionListener = builder.getEvictionListener(isAsync);
data = new ConcurrentHashMap<>(builder.getInitialCapacity());
writeBuffer = new MpscGrowableArrayQueue<>(WRITE_BUFFER_MIN, WRITE_BUFFER_MAX);
readBuffer = evicts() || collectKeys() || collectValues() || expiresAfterAccess()
? new BoundedBuffer<>()
: Buffer.disabled();
accessPolicy = (evicts() || expiresAfterAccess())
? node -> onAccess(node, /* quietly= */ false)
: node -> {};
if (evicts()) {
setMaximumSize(builder.getMaximum());
}
}
/** Ensures that the node is alive during the map operation. */
void requireIsAlive(Object key, Node<?, ?> node) {
if (!node.isAlive()) {
throw new IllegalStateException(brokenEqualityMessage(key, node));
}
}
/** Logs if the node cannot be found in the map but is still alive. */
void logIfAlive(Node<?, ?> node) {
if (node.isAlive()) {
String message = brokenEqualityMessage(node.getKeyReference(), node);
logger.log(Level.ERROR, message, new IllegalStateException());
}
}
/** Returns the formatted broken equality error message. */
String brokenEqualityMessage(Object key, Node<?, ?> node) {
return String.format(US, "An invalid state was detected, occurring when the key's equals or "
+ "hashCode was modified while residing in the cache. This violation of the Map "
+ "contract can lead to non-deterministic behavior (key: %s, key type: %s, "
+ "node type: %s, cache type: %s).", key, key.getClass().getName(),
node.getClass().getSimpleName(), getClass().getSimpleName());View on GitHub (pinned to 9da6581ee3)
Solutions
- Make cache keys immutable (final fields, no setters, immutable collections); derive the key from immutable identity fields only
- If a key's identity must change, remove the entry first, mutate, then reinsert under the new key
- Use an immutable surrogate key (id string, record, UUID) instead of the mutable domain object
- Audit any code that runs inside load/refresh for accidental mutation of the key object
Example fix
// before
class User { String id; String email; /* equals/hashCode use both */ }
cache.put(user, role);
user.setEmail("new@x.com"); // hashCode changed while cached
cache.getIfPresent(user); // IllegalStateException
// after
record UserKey(String id) {}
cache.put(new UserKey(user.id()), role);
user.setEmail("new@x.com"); // key unaffected Defensive patterns
Strategy: validation
Validate before calling
// Before caching, assert the key type is immutable by design (compile-time via records):
// - prefer: record UserId(String id) {} as key
// Runtime smoke test that a copied key still matches after mutation of a value object:
var k1 = new UserKey("u-1");
var k2 = new UserKey("u-1");
assert k1.equals(k2) && k1.hashCode() == k2.hashCode();
cache.put(k1, role);
assert cache.getIfPresent(k2).isPresent(); // equal-but-distinct instance resolves Type guard
// Use only immutable key types; in modern Java prefer records:
record UserKey(String id) {} // equals/hashCode fixed at construction
// If a class must be mutable, gate cache insertion on an immutability marker:
static boolean isSafeCacheKey(Object key) {
return key instanceof String || key instanceof Number
|| key instanceof UUID || key instanceof Record;
} Try / catch
// Prefer prevention over catching: this exception indicates already-corrupted state.
// If observed in production logs, isolate the failing key type and stop mutating it:
try {
var v = cache.getIfPresent(key);
} catch (IllegalStateException e) {
if (e.getMessage() != null && e.getMessage().contains("equals or hashCode")) {
logger.error("Mutable cache key detected: {}", key.getClass(), e);
cache.invalidate(key); // best-effort eviction of the poisoned entry
}
throw e;
} Prevention
- Use records, Strings, UUIDs, or other immutable values as keys
- Never insert arrays, mutable collections, or mutable POJOs as keys
- In code review, flag any setter reachable on a type used as a cache key
- If key state must change, remove-then-reinsert with the new key
When it happens
Trigger: Inserting a mutable object as a cache key and then changing fields used by equals/hashCode; using an array or list as a key; keys whose hashCode depends on mutable state; computing a value that mutates the key during load/refresh.
Common situations: Entities with setter-mutated fields used as keys; Lombok/@Data objects modified after caching; mutable collections or Date/Instant-wrapping objects as keys; a serialization round-trip that changes hashCode (e.g. overridden hashCode inconsistent across JVMs).
Related errors
- throw new ExceptionInInitializerError(e)
- Proxy required
- Invalid option
- key %s value was set to %s, must be an integer
- key %s value was set to %s, must be a long
AI-assisted analysis of ben-manes/caffeine@9da6581ee3 (2026-08-14).
Data as JSON: /api/errors/7037fae91555d30c.
Report an issue: GitHub.