Caching Strategies with Memoization in Clojure

Learn when Clojure's `memoize` is enough, when it is too blunt, and how to think about key shape, eviction, staleness, and concurrency before turning repeated work into cached state.

Memoization: Caching a function result so repeated calls with the same arguments can reuse prior work instead of recomputing it.

Memoization is attractive because it looks almost free:

1(def fast-fib (memoize slow-fib))

But real caching decisions are never free. Once a cache exists, you now own:

  • key design
  • memory growth
  • staleness
  • invalidation
  • concurrency behavior

So the right question is not “can I memoize this?” It is “is caching the right model for this work?”

Built-In memoize Is a Narrow Tool

Clojure’s built-in memoize is process-local and unbounded. That makes it excellent for a small set of cases and risky for many others.

It is strongest when:

  • the function is pure
  • repeated calls reuse a small, stable key space
  • results are expensive enough to compute again
  • unbounded growth is acceptable or naturally limited

It is much weaker when:

  • arguments are huge or highly variable
  • results go stale quickly
  • the input space is effectively unbounded
  • memory pressure matters

In other words, built-in memoize is best for deterministic computational reuse, not as a drop-in application cache for everything expensive.

Cache Keys Deserve as Much Thought as Cache Values

A cache keyed by a giant nested request map is usually worse than a cache keyed by a stable compact identifier. Review:

  • cardinality
  • key normalization
  • inclusion of irrelevant fields
  • whether identity, tenant, version, and locale matter

Bad key shape is one of the fastest ways to get a cache that barely hits and still consumes memory.

1(defn report-key [{:keys [tenant-id report-date locale]}]
2  [tenant-id report-date locale])

This kind of compact key is easier to reason about than caching directly on a huge request map with irrelevant fields.

Staleness and Invalidation Are the Real Design Work

Memoization is easiest when the function is timeless. Many business functions are not. If data changes underneath the function, then the cache needs a policy:

  • TTL
  • explicit invalidation
  • versioned keys
  • rebuild-on-write or rebuild-on-read

That is why generic memoization is rarely the whole answer for database-backed or API-backed results.

Once a result depends on mutable outside state, the design question changes from “can I remember it?” to “what makes this remembered value still valid?”

Concurrency Changes the Cost Model

In a concurrent service, caching raises new questions:

  • do multiple callers compute the same missing value at once?
  • can cache misses stampede a dependency?
  • are values safe to share across threads?
  • is eviction synchronized reasonably?

Clojure’s concurrency story helps with safe sharing of immutable values, but it does not remove cache coordination concerns.

One of the most common failures is a cache miss stampede, where many callers all compute the same missing value at once. Sharing immutable results is easy; coordinating the miss path is the hard part.

Cache Scope Matters

Before picking a caching mechanism, decide where the cache actually belongs:

  • request-local reuse
  • one process or node
  • shared distributed cache
  • persistent derived data in a database or index

Those scopes solve different problems. Using process-local memoization for data that must stay coherent across many instances is often the wrong model from the beginning.

Memoization Is a CPU-for-Memory Trade

That trade is often worth it. But make it explicit. A cache that saves CPU by consuming unbounded memory is not automatically an optimization.

Common Failure Modes

Memoizing Impure Functions

If the result depends on time, I/O, or mutable external state, generic memoization can become wrong quickly.

Using Unbounded Caches for High-Cardinality Inputs

That turns performance optimization into a memory-retention bug.

Forgetting Invalidation Rules

The cache then becomes a stale data delivery system.

Caching Before Measuring Reuse

If the same key rarely appears twice, the cache adds complexity with little benefit.

Practical Heuristics

Use built-in memoize only for pure functions with compact keys and naturally bounded reuse. For real application caches, decide the right scope first, then think carefully about key design, eviction, invalidation, and miss coordination. In Clojure, immutable values make sharing cache entries safer, but they do not make caching policy trivial.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026