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

  1. Make cache keys immutable (final fields, no setters, immutable collections); derive the key from immutable identity fields only
  2. If a key's identity must change, remove the entry first, mutate, then reinsert under the new key
  3. Use an immutable surrogate key (id string, record, UUID) instead of the mutable domain object
  4. 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

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


AI-assisted analysis of ben-manes/caffeine@9da6581ee3 (2026-08-14). Data as JSON: /api/errors/7037fae91555d30c. Report an issue: GitHub.