This guide covers all the ways to clear and invalidate cache entries in Memento.
Cached data becomes stale when the underlying data changes. Good invalidation ensures users see fresh data while still benefiting from caching. Memento provides several invalidation strategies:
(require '[memento.core :as m])
(m/memo-clear! get-user) ; Clears all cached results for get-user
Pass the same arguments that were used to cache the value:
;; Clear the cached result for (get-user 123)
(m/memo-clear! get-user 123)
;; For multi-argument functions
(m/memo-clear! get-user-orders 123 :pending) ; Clears (get-user-orders 123 :pending)
If multiple functions share a cache, clear all entries at once:
(def shared-cache (m/create {mc/type mc/caffeine mc/size< 10000}))
(m/bind #'get-user {} shared-cache)
(m/bind #'get-user-orders {} shared-cache)
;; Clears entries from BOTH functions
(m/memo-clear-cache! shared-cache)
The real power of Memento is invalidating related data across multiple functions with a single call. This uses a secondary index that maps entity IDs to cache entries.
In a typical application you have:
get-user, get-user-orders, get-user-preferences)update-user!, delete-user!, merge-users!)Without tag-based invalidation, every modifying function must know about every cached function:
;; Every modifier must list ALL cached functions - maintenance nightmare!
(defn update-user! [user-id data]
(db/update-user! user-id data)
(m/memo-clear! get-user user-id)
(m/memo-clear! get-user-orders user-id)
(m/memo-clear! get-user-preferences user-id)
;; Did we forget one? Will we remember to add new ones?
)
This creates an N×M maintenance burden:
Tag-based invalidation decouples producers from consumers. They only need to agree on a tag name:
;; CACHED FUNCTIONS: 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)))
;; MODIFYING FUNCTIONS: 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)) ; Clears ALL :user-tagged caches
Now you can add cached functions or modifiers independently.
(m/defmemo get-user
{mc/type mc/caffeine
mc/tags [:user]} ; This function participates in :user invalidation
[user-id]
(db/fetch-user user-id))
(m/defmemo get-user-orders
{mc/type mc/caffeine
mc/tags [:user]}
[user-id]
(db/fetch-orders user-id))
Use m/with-tag-id to associate cached values with entity IDs:
(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))) ; "This result is about 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)))
;; Clears ALL entries tagged with [:user 123] from ALL :user-tagged functions
(m/memo-clear-tag! :user 123)
A cached value can be tagged with multiple entity IDs. This is essential for aggregated data like dashboards.
(m/defmemo get-order
{mc/type mc/caffeine
mc/tags [:user :order]}
[order-id]
(let [order (db/fetch-order order-id)]
(-> order
(m/with-tag-id :order order-id)
(m/with-tag-id :user (:user-id order)))))
;; Now you can invalidate by either:
(m/memo-clear-tag! :order 456) ; Clear this specific order
(m/memo-clear-tag! :user 123) ; Clear all orders for user 123
Consider a dashboard showing the last 10 users who logged in. If any of those users is modified, the dashboard cache should be invalidated:
(m/defmemo get-recent-users-dashboard
{mc/type mc/caffeine
mc/tags [:user]}
[]
(let [users (db/fetch-recent-users 10)]
;; Tag with ALL user IDs that appear in this cached result
(reduce (fn [result user]
(m/with-tag-id result :user (:id user)))
{:users users :generated-at (java.time.Instant/now)}
users)))
;; Now if ANY of those 10 users is modified:
(defn update-user! [user-id data]
(db/update-user! user-id data)
(m/memo-clear-tag! :user user-id)) ; Clears dashboard if this user was in it
The dashboard is automatically invalidated when any user it displays is modified, but NOT when unrelated users are modified.
ret-fn for Cleaner CodeInstead of adding with-tag-id inside your function, use ret-fn to separate caching concerns:
(defn tag-user-data [[user-id] result]
(m/with-tag-id result :user user-id))
(m/defmemo get-user
{mc/type mc/caffeine
mc/tags [:user]
mc/ret-fn tag-user-data}
[user-id]
(db/fetch-user user-id)) ; Clean function, no caching logic
For better atomicity when invalidating multiple entities:
;; Invalidate multiple tag+id pairs atomically
(m/memo-clear-tags! [:user 123] [:user 456] [:order 789])
This ensures all invalidations happen together, preventing race conditions where some data is cleared but related data isn't.
You can pre-populate or manually update cache entries:
;; Add entries to a function's cache
;; Keys are argument vectors, values are the cached results
(m/memo-add! get-user {[123] {:id 123 :name "Alice"}
[456] {:id 456 :name "Bob"}})
This is useful for:
Use m/do-not-cache to prevent certain results from being cached:
(m/defmemo get-user
{mc/type mc/caffeine}
[user-id]
(if-let [user (db/fetch-user user-id)]
user
(m/do-not-cache nil))) ; Don't cache "not found" results
Or use ret-fn for cleaner separation:
(defn no-cache-errors [_ response]
(if (>= (:status response) 400)
(m/do-not-cache response)
response))
(m/defmemo fetch-api-data
{mc/type mc/caffeine
mc/ret-fn no-cache-errors}
[endpoint]
(http/get endpoint))
Use if-cached to check without triggering a cache miss:
(m/if-cached [user (get-user 123)]
(println "User was cached:" user)
(println "User not in cache"))
Update cache after successful writes:
(defn update-user! [user-id data]
(let [updated-user (db/update-user! user-id data)]
;; Option 1: Invalidate and let next read refresh
(m/memo-clear-tag! :user user-id)
;; Option 2: Update cache directly (write-through)
(m/memo-add! get-user {[user-id] updated-user})
updated-user))
Invalidate based on events from a message queue:
(defn handle-event [event]
(case (:type event)
:user-updated (m/memo-clear-tag! :user (:user-id event))
:order-completed (m/memo-clear-tags!
[:order (:order-id event)]
[:user (:user-id event)])
nil))
;; Get all mount points for a tag
(m/mounts-by-tag :user)
;; Clear all caches for a tag (without specifying an ID)
(doseq [mp (m/mounts-by-tag :user)]
(m/memo-clear! mp))
If multiple threads request the same uncached key simultaneously, only one actually calls the function. The others wait and receive the same result.
If a key is invalidated while being loaded, the load is retried to ensure fresh data. The loading thread is interrupted when a tag is invalidated.
The hardest concurrency challenge with caching is invalidating call trees - cached functions that call other cached functions.
Consider this scenario:
(m/defmemo get-user-summary [user-id] ; Calls get-user-details
(let [details (get-user-details user-id)]
(summarize details)))
(m/defmemo get-user-details [user-id] ; Lower-level cache
(db/fetch-user user-id))
When you invalidate both caches, there's a race condition:
get-user-summary for user 123get-user-details, another thread calls get-user-summaryget-user-details, which still has stale dataget-user-summaryget-user-details - but get-user-summary now has stale dataWhen using tag-based invalidation (memo-clear-tag!), Memento uses locking mechanisms to coordinate invalidation across all tagged functions. This is a best-effort solution - it significantly reduces the window for race conditions but cannot eliminate them entirely in all edge cases.
;; Tag-based invalidation coordinates across functions
(m/memo-clear-tag! :user user-id)
Request-scoped caching sidesteps the problem entirely: each request starts with a fresh cache and discards it at the end. Within a request, you can be very liberal with cache clearing - just nuke everything after any write operation:
(defn handle-request [request]
(m/with-caches :request (constantly (m/create {mc/type mc/caffeine}))
;; ... do reads, cache is populated ...
(when (write-operation? request)
;; After any DB write, just clear the entire request cache
;; No need to be precise - it's cheap and guarantees correctness
(m/memo-clear-cache! (m/active-cache get-user))
;; Or clear all caches for the tag:
(doseq [mp (m/mounts-by-tag :request)]
(m/memo-clear! mp)))
;; ... continue with fresh data ...
))
You sacrifice some caching performance (entries you could have kept are cleared), but you gain simplicity and correctness. Since the cache only lives for one request anyway, the cost is limited. This is much easier than tracking exactly which cached functions are affected by each write.
For long-lived caches, if you can afford to clear all cached data (not just one entity), backing related functions with a shared cache and clearing it atomically eliminates the race condition:
(def user-cache (m/create {mc/type mc/caffeine mc/size< 10000}))
(m/bind #'get-user-details {} user-cache)
(m/bind #'get-user-summary {} user-cache)
;; Clears ALL entries for ALL functions - atomic, no race condition
(m/memo-clear-cache! user-cache)
This is a sledgehammer approach - it clears everything, not just user 123's data. Only practical when you genuinely need to invalidate all cached data.
Most caching libraries don't address the call tree problem at all. Memento's tag-based invalidation with lockout coordination handles the common cases correctly.
See Internals for details on the lockout mechanism.
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 |