A Clojure memoization library with scoped caching and smart invalidation.
clojure.core/memoize and clojure.core.memoize provide basic caching, but real applications need:
Traditional caching strategies struggle with API/web requests:
What you actually want for request handling:
Memento's with-caches makes this trivial. While request handling is the most common use case, scopes work for any bounded context - background jobs, batch processing, test fixtures - and can be nested.
When a user updates their profile, you need to invalidate all cached data about that user - across multiple functions. Memento's tag-based invalidation lets you do this with a single call:
(m/memo-clear-tag! :user user-id) ; Clears user 123's data from ALL tagged caches
You have single-item cached functions like get-user-email. Elsewhere you load a list of 100 users. Without coordination, calling get-user-email for each user means 100 database queries - for data you already have.
Memento's event system lets bulk loaders populate single-item caches:
;; After loading users in bulk, fire events to populate individual caches
(doseq [user users]
(m/fire-event! :user [:user-seen user]))
See Events documentation for the full pattern.
;; deps.edn
org.clojars.roklenarcic/memento {:mvn/version "2.0.68"}
;; Leiningen
[org.clojars.roklenarcic/memento "2.0.68"]
Requires Java 11+.
(require '[memento.core :as m]
'[memento.config :as mc])
;; Basic memoization - wrap your function with a cache
(def get-user
(m/memo (fn [user-id]
(println "Fetching user" user-id)
{:id user-id :name "Alice"})
{mc/type mc/caffeine})) ; Use Caffeine cache
(get-user 1) ; prints "Fetching user 1", returns {:id 1 :name "Alice"}
(get-user 1) ; returns cached result, no print
defmemodefmemo works just like defn - the map is standard function metadata:
(require '[memento.core :as m]
'[memento.config :as mc])
(m/defmemo get-user
"Fetches user from database."
{mc/type mc/caffeine}
[user-id]
(db/fetch-user user-id))
(m/defmemo get-user
"Fetches user, cached for 5 minutes, max 1000 entries."
{mc/type mc/caffeine
mc/size< 1000
mc/ttl [5 :m]}
[user-id]
(db/fetch-user user-id))
Note: Include mc/type mc/caffeine for functions that should always cache. For request-scoped caching, you can omit the type (see below).
Data goes stale. Set a TTL (time-to-live) to automatically expire entries:
(m/defmemo get-exchange-rate
"Cache exchange rates for 1 minute."
{mc/type mc/caffeine
mc/ttl [1 :m]} ; Also: [30 :s], [2 :h], [1 :d]
[currency]
(api/fetch-rate currency))
Or use fade for access-based expiration (expires if not accessed):
(m/defmemo get-user-preferences
"Cache preferences, expire after 10 minutes of no access."
{mc/type mc/caffeine
mc/fade [10 :m]}
[user-id]
(db/fetch-preferences user-id))
Prevent unbounded memory growth with size limits:
(m/defmemo get-product
"Cache up to 10,000 products (LRU eviction)."
{mc/type mc/caffeine
mc/size< 10000}
[product-id]
(db/fetch-product product-id))
Use with-caches to temporarily replace caches for tagged functions within a scope:
;; Option 1: No caching outside scope (tags only, no mc/type)
(m/defmemo get-user
{mc/tags [:request]}
[user-id]
(db/fetch-user user-id))
;; Option 2: Long-term cache outside scope, fresh cache inside
(m/defmemo get-user-orders
{mc/type mc/caffeine
mc/ttl [1 :h]
mc/tags [:request]}
[user-id]
(db/fetch-orders user-id))
;; In your request handler middleware
(defn wrap-request-cache [handler]
(fn [request]
(m/with-caches :request
(constantly (m/create {mc/type mc/caffeine})) ; Fresh cache for this scope
(handler request))))
;; Within a request:
;; - Both functions use the fresh scoped cache
;; - Multiple calls with same args hit the cache
;; - Cache is discarded when scope ends
See the Scoped Caching Guide for more patterns including nested scopes and consulting long-term caches.
By default, the cache key is the full argument list. Use mc/key-fn to transform it:
;; Ignore the db-conn argument for caching purposes
(m/defmemo get-user
{mc/type mc/caffeine
mc/key-fn rest} ; Cache key is [user-id], not [db-conn user-id]
[db-conn user-id]
(db/fetch-user db-conn user-id))
;; Extract a nested value from a request map
(m/defmemo get-current-user
{mc/type mc/caffeine
mc/key-fn* (fn [request] (-> request :session :user-id))}
[request]
(db/fetch-user (-> request :session :user-id)))
mc/key-fn receives args as a sequence; mc/key-fn* receives them as separate parameters (like the function itself).
Use mc/ret-fn to transform values before caching, or prevent caching certain values:
;; Don't cache error responses
(m/defmemo fetch-api-data
{mc/type mc/caffeine
mc/ret-fn (fn [args response]
(if (>= (:status response 0) 400)
(m/do-not-cache response) ; Don't cache errors
response))}
[endpoint]
(http/get endpoint))
Without tag-based invalidation, you face an N×M maintenance problem:
Tag-based invalidation decouples them completely:
;; CACHED FUNCTIONS: just tag with :user, don't care who invalidates
(m/defmemo get-user
{mc/type mc/caffeine, mc/tags [:user]}
[user-id]
(-> (db/fetch-user user-id)
(m/with-tag-id :user user-id)))
(m/defmemo get-user-orders
{mc/type mc/caffeine, mc/tags [:user]}
[user-id]
(-> (db/fetch-orders user-id)
(m/with-tag-id :user user-id)))
(m/defmemo get-user-preferences
{mc/type mc/caffeine, mc/tags [:user]}
[user-id]
(-> (db/fetch-preferences user-id)
(m/with-tag-id :user user-id)))
;; MODIFYING FUNCTIONS: just invalidate :user tag, don't care who's cached
(defn update-user! [user-id data]
(db/update-user! user-id data)
(m/memo-clear-tag! :user user-id))
(defn delete-user! [user-id]
(db/delete-user! user-id)
(m/memo-clear-tag! :user user-id))
(defn merge-users! [from-id to-id]
(db/merge-users! from-id to-id)
(m/memo-clear-tags! [:user from-id] [:user to-id]))
Now you can add cached functions or modifying functions independently - they only need to agree on the tag name (:user).
A cached value can also be tagged with multiple IDs - useful for aggregated data like dashboards. See the Invalidation Guide for details.
;; Clear all entries for a function
(m/memo-clear! get-user)
;; Clear specific entry
(m/memo-clear! get-user 123)
Durations can be numbers (seconds) or [amount :unit] pairs:
30 ; 30 seconds
[30 :s] ; 30 seconds
[5 :m] ; 5 minutes
[2 :h] ; 2 hours
[1 :d] ; 1 day
| Setting | Description | Example |
|---|---|---|
mc/type | Cache implementation (required) | mc/caffeine |
mc/size< | Max entries (LRU eviction) | 1000 |
mc/ttl | Time-to-live | [5 :m] |
mc/fade | Expire after last access | [10 :m] |
mc/tags | Tags for scoping/invalidation | [:user :request] |
mc/key-fn | Transform args to cache key | (fn [args] ...) |
mc/ret-fn | Transform return value | (fn [args val] ...) |
See Configuration Guide for all options.
key-fn, ret-fnwith-caches, nested scopes, request patternsFor testing or debugging, disable all caching globally:
java -Dmemento.enabled=false ...
See MIGRATION.md for version upgrade guides.
Copyright 2020-2024 Rok Lenarcic
Licensed under the MIT License.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |