This guide covers Memento's advanced features for complex caching scenarios.
Tiered caching combines multiple caches, typically a fast local cache in front of a slower but larger upstream cache.
Memento provides three combining strategies:
m/tiered - Both Updated(m/tiered local-cache upstream-cache)
(def local-cache (m/create {mc/type mc/caffeine mc/size< 100 mc/ttl [1 :m]}))
(def redis-cache (create-redis-cache)) ; Hypothetical Redis implementation
(m/defmemo get-user
{mc/type mc/caffeine}
[user-id]
(db/fetch-user user-id))
(m/bind #'get-user {} (m/tiered local-cache redis-cache))
m/consulting - Local Updated Only(m/consulting local-cache upstream-cache)
Best for request-scoped caching (see Scoped Caching Guide).
m/daisy - Upstream Updated Only(m/daisy local-cache upstream-cache)
;; Pre-loaded cache with default values
(def defaults-cache
(m/create {mc/type mc/caffeine
mc/seed {[:theme] "light"
[:language] "en"}}))
(def user-prefs-cache (m/create {mc/type mc/caffeine mc/ttl [1 :h]}))
(m/defmemo get-preference
{mc/type mc/caffeine}
[key]
(db/fetch-preference key))
;; Check defaults first, fall back to user preferences
(m/bind #'get-preference {} (m/daisy defaults-cache user-prefs-cache))
Invalidation operations affect both caches in tiered setups:
(m/memo-clear! get-user 123) ; Clears from both local AND upstream
Other operations (like as-map) only affect the local cache.
Events solve the N+1 query problem for single-item cached functions. When you load a list of N items, you can populate N cache entries - avoiding N future database queries.
You have a function that fetches one user's email by ID. It's cached, so repeated calls are fast. But what happens when you load a list of 100 users elsewhere?
;; Bulk load - returns [{:id 1 :email "alice@example.com"} {:id 2 :email "bob@example.com"} ...]
(defn get-all-users []
(db/fetch-all-users))
;; Individual lookups (cached)
(m/defmemo get-user-email
{mc/type mc/caffeine}
[user-id]
(db/fetch-user-email user-id))
After calling get-all-users, you already have every user's email in memory. But get-user-email doesn't know that. When you later call (get-user-email 1), (get-user-email 2), etc., each one hits the database - 100 extra queries for data you already had.
Use mc/evt-fn to define how a function should handle incoming events, then fire-event! to broadcast data:
(require '[memento.core :as m]
'[memento.config :as mc])
;; Individual lookup with event handler
(m/defmemo get-user-email
{mc/type mc/caffeine
mc/tags [:user]
mc/evt-fn (m/evt-cache-add
:user-seen
(fn [{:keys [id email]}]
{[id] email}))} ; Maps event payload to cache entry
[user-id]
(db/fetch-user-email user-id))
;; Bulk load fires events to warm the cache
(defn get-all-users []
(let [users (db/fetch-all-users)]
;; Fire event for each user to all :user-tagged functions
(doseq [u users]
(m/fire-event! :user [:user-seen u]))
users))
Now when get-all-users loads 100 users, it fires 100 events. Each :user-tagged function receives those events and populates its cache. Subsequent calls to (get-user-email 1) return instantly from cache.
The real power comes when you have multiple cached functions for the same entity:
;; All these functions are tagged :user and handle :user-seen events
(m/defmemo get-user-email
{mc/type mc/caffeine
mc/tags [:user]
mc/evt-fn (m/evt-cache-add :user-seen
(fn [{:keys [id email]}] {[id] email}))}
[user-id]
(db/fetch-user-email user-id))
(m/defmemo get-user-name
{mc/type mc/caffeine
mc/tags [:user]
mc/evt-fn (m/evt-cache-add :user-seen
(fn [{:keys [id name]}] {[id] name}))}
[user-id]
(db/fetch-user-name user-id))
(m/defmemo get-user-role
{mc/type mc/caffeine
mc/tags [:user]
mc/evt-fn (m/evt-cache-add :user-seen
(fn [{:keys [id role]}] {[id] role}))}
[user-id]
(db/fetch-user-role user-id))
One fire-event! call broadcasts to all :user-tagged functions. Each extracts what it needs from the payload.
evt-cache-add Helperevt-cache-add creates an event handler that:
(m/evt-cache-add
:event-type ; Only handle [:event-type payload] events
(fn [payload] ; Transform payload to cache entries
{[arg1 arg2] value ; Map of [args] -> cached-value
[arg3] value2})) ; Can return multiple entries
;; Fire to all functions tagged with :user
(m/fire-event! :user [:user-seen {:id 1 :email "alice@example.com" :name "Alice"}])
;; Fire to a specific function only
(m/fire-event! get-user-email [:user-seen {:id 1 :email "alice@example.com"}])
For complex scenarios (invalidation, logging, conditional caching), write your own handler:
(defn my-event-handler [mount-point event]
(let [[event-type payload] event]
(case event-type
:user-seen
(m/memo-add! mount-point {[(:id payload)] (:email payload)})
:user-deleted
(m/memo-clear! mount-point (:id payload))
nil))) ; Unknown events ignored
(m/defmemo get-user-email
{mc/type mc/caffeine
mc/evt-fn my-event-handler}
[user-id]
(db/fetch-user-email user-id))
Tags serve three purposes in Memento:
with-caches enables caching for tagged functionsmemo-clear-tag! clears entries by tag + IDfire-event! broadcasts data to tagged functionsTogether they let you build efficient caching without tight coupling between functions.
Instead of fixed TTL/fade for all entries, set expiry per-entry based on the value.
Expiry InterfaceImplement memento.caffeine.Expiry:
(import '[memento.caffeine Expiry])
;; Cache downstream service responses:
;; - Success (2xx): cache for 1 hour
;; - Server errors (5xx): cache briefly to avoid hammering a failing service
(def service-response-expiry
(reify Expiry
(ttl [this segment key response]
(if (>= (:status response) 500)
[5 :m] ; Errors: cache 5 minutes, then retry
[1 :h])) ; Success: cache 1 hour
(fade [this segment key value]
nil)))
(m/defmemo call-downstream-service
{mcc/expiry service-response-expiry}
[endpoint]
(http/get endpoint))
The Expiry interface has two methods:
ttl [this segment key value] - Return TTL duration or nilfade [this segment key value] - Return fade duration or nilReturn nil to use the cache's base ttl or fade setting.
A built-in implementation reads expiry from value metadata:
(require '[memento.caffeine.config :as mcc])
;; OAuth tokens - cache until they expire
(m/defmemo get-access-token
{mcc/expiry mcc/meta-expiry}
[client-id]
(let [token (oauth/fetch-token client-id)
expires-in (:expires_in token)] ; seconds until expiry
(with-meta token
{mc/ttl [(- expires-in 60) :s]}))) ; refresh 1 minute early
Set mc/ttl and/or mc/fade in metadata to control expiry.
Automatically attach caches to annotated functions across namespaces.
(ns myapp.users
(:require [memento.core :as m]
[memento.config :as mc]))
;; Add ::m/cache to function metadata
(defn ^{::m/cache {mc/ttl [5 :m]}} get-user
[user-id]
(db/fetch-user user-id))
;; Or use defn's metadata syntax
(defn get-user-orders
{::m/cache {mc/ttl [10 :m] mc/tags [:user]}}
[user-id]
(db/fetch-orders user-id))
(require '[memento.ns-scan :as ns-scan])
;; Scan all loaded namespaces and attach caches
(ns-scan/attach-caches)
This finds all vars with ::m/cache metadata and calls m/memo on them.
;; Custom namespace filter (default excludes clojure.* and nrepl.*)
(ns-scan/attach-caches {:blacklist #"^(clojure|nrepl|myapp\.internal)\..*"})
Note: Only works on already-loaded namespaces. Ensure namespaces are required before calling attach-caches.
One cache can back multiple functions, sharing size limits:
;; Single cache with 10,000 entry limit
(def user-data-cache (m/create {mc/size< 10000}))
;; Both functions share the cache
(m/bind #'get-user {} user-data-cache)
(m/bind #'get-user-preferences {} user-data-cache)
;; Together they can have at most 10,000 entries
;; Entries are evicted based on overall LRU, not per-function
This is useful when:
if-cached ConditionalCheck if a value is cached without triggering a miss:
(m/if-cached [user (get-user 123)]
;; Value was cached
(println "Got cached user:" (:name user))
;; Value was not cached (function NOT called)
(println "User not in cache"))
Use cases:
Instead of counting entries, evict based on total weight:
(require '[memento.caffeine.config :as mcc])
(m/defmemo get-document
{mcc/weight< 100000000 ; 100MB total
mcc/kv-weight (fn [id key value]
(count (:content value)))} ; Weight = content size
[doc-id]
(db/fetch-document doc-id))
Useful when cached values have highly variable sizes.
Allow GC to reclaim cached values under memory pressure:
(require '[memento.caffeine.config :as mcc])
;; Weak values - GC can reclaim anytime
(m/defmemo get-large-object
{mcc/weak-values true}
[id]
(load-large-object id))
;; Soft values - GC reclaims only under memory pressure
(m/defmemo get-medium-object
{mcc/soft-values true}
[id]
(load-medium-object id))
Use for:
Get notified when entries are evicted:
(require '[memento.caffeine.config :as mcc])
(m/defmemo get-resource
{mc/size< 100
mcc/removal-listener (fn [id key value cause]
(println "Evicted" key "because" cause)
(when (= cause :explicit)
(cleanup-resource value)))}
[resource-id]
(acquire-resource resource-id))
Causes: :explicit, :replaced, :collected, :expired, :size
Enable and retrieve cache statistics:
(require '[memento.caffeine.config :as mcc])
(m/defmemo get-user
{mcc/stats true}
[user-id]
(db/fetch-user user-id))
;; After some usage...
(m/stats get-user)
;; => {:hit-count 1523
;; :miss-count 234
;; :eviction-count 12
;; ...}
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 |