This guide explains how to use Memento's scoped caching feature. While request handling is the most common use case, scopes can be used for any bounded context - background jobs, batch processing, test fixtures, etc. Scopes can also be nested.
When building APIs or web applications, traditional caching approaches have fundamental problems:
With TTL (time-to-live) caching, you must choose an expiration time. But what's the right value?
The core problem is that data can change at any moment. There's no "safe" TTL that balances freshness and performance. You're always making a tradeoff, and often getting it wrong in one direction or the other.
Size limits (LRU eviction) prevent memory from growing unbounded, but they don't address staleness at all. A cached entry could be:
You have no guarantees. Size-based caching is about memory management, not data freshness.
For request handling, the ideal caching behavior is:
This gives you the performance benefit of caching (no repeated DB calls within a request) without the staleness problem (each request sees current data).
with-cachesMemento's with-caches macro temporarily replaces caches for tagged functions within a scope.
(m/with-caches :tag-name cache-factory-fn
body...)
The cache-factory-fn is called for each tagged mount point with its current cache as the argument. The returned cache is used for that function within the scope. Most commonly, you use constantly to share one cache across all tagged functions:
;; All :request-tagged functions share this one cache
(m/with-caches :request
(constantly (m/create {mc/type mc/caffeine}))
(handle-request request))
But you can also create per-function caches or make decisions based on the existing cache:
;; Each tagged function gets its own cache with a size limit
(m/with-caches :request
(fn [existing-cache] (m/create {mc/type mc/caffeine mc/size< 100}))
(handle-request request))
;; Wrap existing cache with consulting pattern
(m/with-caches :request
(fn [existing-cache]
(m/create (m/consulting {mc/type mc/caffeine} existing-cache)))
(handle-request request))
Define functions with tags but no cache type. They won't cache outside scopes:
(require '[memento.core :as m]
'[memento.config :as mc])
;; Tags only, no mc/type = no caching by default
(m/defmemo get-user
{mc/tags [:request]}
[user-id]
(db/fetch-user user-id))
;; Outside any scope: no caching (safe default)
;; Inside with-caches: uses the provided cache
(m/with-caches :request
(constantly (m/create {mc/type mc/caffeine}))
(handle-request request))
Define functions with a long-term cache, then swap to a fresh cache within requests:
;; Function has a long-term cache by default
(m/defmemo get-user
{mc/type mc/caffeine
mc/ttl [1 :h]
mc/tags [:request]}
[user-id]
(db/fetch-user user-id))
;; Outside scope: uses the long-term cache (1 hour TTL)
;; Inside scope: uses a fresh request-scoped cache
(m/with-caches :request
(constantly (m/create {mc/type mc/caffeine}))
(handle-request request))
Both patterns work. Choose based on whether you want caching outside of scopes.
Inside with-caches:
Often you want request-scoped caching but also want to benefit from a long-term cache. Use m/consulting:
;; Long-term cache for users (1 hour TTL)
(def user-cache (m/create {mc/type mc/caffeine mc/ttl [1 :h] mc/size< 10000}))
;; Define with tags only - no mc/type
(m/defmemo get-user
{mc/tags [:request]}
[user-id]
(db/fetch-user user-id))
;; Bind to the long-term cache by default (used outside request scope)
(m/bind #'get-user {} user-cache)
;; Middleware that creates a request cache consulting the long-term cache
(defn wrap-request-cache [handler]
(fn [request]
(m/with-caches :request
(fn [existing-cache]
;; Create a request cache that CONSULTS the long-term cache
(m/create (m/consulting {mc/type mc/caffeine} existing-cache)))
(handler request))))
With consulting:
This is the recommended pattern for most web applications.
Scopes can be nested. The innermost with-caches for a given tag wins. This is useful for:
(m/defmemo get-user
{mc/tags [:batch :request]} ; Participates in both scopes
[user-id]
(db/fetch-user user-id))
;; Outer scope for batch job (cache shared across all items)
(m/with-caches :batch
(constantly (m/create {mc/type mc/caffeine mc/size< 10000}))
(doseq [item items]
;; Inner scope for each item (fresh cache per item)
(m/with-caches :request
(constantly (m/create {mc/type mc/caffeine}))
(process-item item))))
Functions can have multiple tags, letting them participate in different scoping strategies depending on which scope is active.
If you need to permanently change the cache for all tagged functions (not just within a scope), use update-tag-caches!:
;; Replace ALL :request-tagged caches with new empty caches
(m/update-tag-caches! :request (constantly (m/create {mc/type mc/caffeine})))
This is useful for:
;; Get all mount points tagged with :request
(m/mounts-by-tag :request)
;; => #{#memento.mount.TaggedMountPoint{...} ...}
;; Get the caches being used
(m/caches-by-tag :request)
;; => [#memento.caffeine.CaffeineCache{...} ...]
(m/tags get-user)
;; => [:request :user]
with-caches WorksThis means:
with-caches blocks work correctlyconstantly makes all tagged functions share one cacheWhen combining request-scoped and long-term caches, you have three options:
| Type | Use Case | After Miss |
|---|---|---|
m/consulting | Request cache in front of long-term | Entry in request cache only |
m/tiered | Local cache in front of external (Redis) | Entry in both caches |
m/daisy | Pre-loaded cache with fallback | Entry in upstream only |
consulting (Most Common for Request Scoping)(m/consulting {} long-term-cache)
tiered(m/tiered {} long-term-cache)
daisy(m/daisy pre-loaded-cache upstream-cache)
(ns myapp.cache
(:require [memento.core :as m]
[memento.config :as mc]))
;; Long-term caches (used outside request scope, or consulted within)
(def user-cache (m/create {mc/type mc/caffeine mc/ttl [1 :h] mc/size< 10000}))
(def product-cache (m/create {mc/type mc/caffeine mc/ttl [5 :m] mc/size< 50000}))
;; Cached functions - tags only, no mc/type
(m/defmemo get-user
{mc/tags [:request :user]}
[user-id]
(db/fetch-user user-id))
(m/bind #'get-user {} user-cache) ; Bind to long-term cache
(m/defmemo get-product
{mc/tags [:request]}
[product-id]
(db/fetch-product product-id))
(m/bind #'get-product {} product-cache)
;; Middleware - provides request-scoped cache that consults long-term
(defn wrap-request-cache [handler]
(fn [request]
(m/with-caches :request
(fn [existing]
(m/create (m/consulting {mc/type mc/caffeine} existing)))
(handler request))))
;; Handler
(defn handle-product-page [request]
(let [user (get-user (:user-id request)) ; Checks request cache,
product (get-product (:product-id request)) ; then consults long-term
user-again (get-user (:user-id request))] ; Hits request cache
(render-page user product)))
In this example:
mc/typewith-caches provides a fresh cache that consults the long-term oneCan 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 |