6 Usage Practices
Last modified: 2026-02-08
This chapter expands on the basics from Getting Started with practical patterns for day-to-day work: function identity in depth, cache invalidation strategies, testing, REPL introspection, serialization constraints, and debugging tips.
Setup
(ns pocket-book.usage-practices
(:require
;; Logging setup for this chapter (see Logging chapter):
[pocket-book.logging]
;; Pocket API:
[scicloj.pocket :as pocket]
;; Annotating kinds of visualizations:
[scicloj.kindly.v4.kind :as kind]))(def test-dir "/tmp/pocket-dev-practices")(pocket/set-base-cache-dir! test-dir)10:06:34.519 INFO scicloj.pocket - Cache dir set to: /tmp/pocket-dev-practices
"/tmp/pocket-dev-practices"When to Use Pocket
Good use cases
Data science pipelines with expensive intermediate steps (data loading, preprocessing, feature engineering, model training)
Reproducible research where cached intermediate results let you iterate on downstream steps without re-running upstream computations
Long-running computations (minutes to hours) that need to survive JVM restarts, crashes, or machine reboots
Multi-threaded workflows where multiple threads may request the same expensive computation β Pocket ensures it runs only once
Comparison to alternatives
| Feature | clojure.core/memoize |
core.memoize |
|
|---|---|---|---|
| Persistence | Disk + memory | Memory only | Memory only |
| Cross-session | Yes | No | No |
| Lazy evaluation | IDeref |
Eager | Eager |
| Eviction policies | LRU, FIFO, TTL, etc. | None | LRU, TTL, etc. |
| Thread-safe (single computation) | Yes | No | Yes |
| Pipeline caching | Yes (recursive) | No | No |
Function Identity: Use Vars or Keywords
Pocket requires functions to be passed as vars (#'fn-name) or keywords (e.g., :train), not as bare function objects.
;; β WRONG - bare function, unstable identity
(pocket/cached my-expensive-fn arg1 arg2)
;; β
CORRECT - var, stable identity
(pocket/cached #'my-expensive-fn arg1 arg2)
;; β
CORRECT - keyword, stable identity
(pocket/cached :train cached-split)Why? Function objects have different identity across JVM sessions, making cache keys unpredictable. Vars provide stable symbol names that survive restarts. Keywords are inherently stable and work naturally as map accessors on cached values.
Pocket validates this and throws a clear error if you forget:
(defn example-fn [x] (* x x))(try
(pocket/cached example-fn 5)
(catch Exception e
(ex-message e)))"pocket/cached requires a var or keyword (e.g., #'my-fn or :my-key), got: class pocket_book.usage_practices$example_fn"Cache Invalidation Strategies
Pocket does not automatically detect when a functionβs implementation changes. You must invalidate manually. Here are the strategies:
Strategy 1: Manual Invalidation
Use invalidate! for specific entries or invalidate-fn! for all entries of a function:
(pocket/cleanup!)10:06:34.524 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed false}(defn transform [x] (* x 2))Cache a computation:
(deref (pocket/cached #'transform 10))10:06:34.525 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/transform
10:06:34.526 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/1c/(pocket-book.usage-practices_transform 10)
20Function implementation changed β invalidate a single entry:
(pocket/invalidate! #'transform 10)10:06:34.528 INFO scicloj.pocket.impl.cache - Invalidated: /tmp/pocket-dev-practices/1c/(pocket-book.usage-practices_transform 10) existed= true
{:path
"/tmp/pocket-dev-practices/1c/(pocket-book.usage-practices_transform 10)",
:existed true}Or invalidate all entries for a function:
(deref (pocket/cached #'transform 1))10:06:34.528 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/transform
10:06:34.529 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/db/(pocket-book.usage-practices_transform 1)
2(deref (pocket/cached #'transform 2))10:06:34.530 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/transform
10:06:34.530 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/e4/(pocket-book.usage-practices_transform 2)
4(pocket/invalidate-fn! #'transform)10:06:34.531 INFO scicloj.pocket.impl.cache - Invalidated 2 entries for pocket-book.usage-practices/transform
{:fn-name "pocket-book.usage-practices/transform",
:count 2,
:paths
["/tmp/pocket-dev-practices/db/(pocket-book.usage-practices_transform 1)"
"/tmp/pocket-dev-practices/e4/(pocket-book.usage-practices_transform 2)"]}Strategy 2: Versioning Pattern
Add a version key to your functionβs input. Bumping the version creates new cache entries while preserving old ones for comparison:
(pocket/cleanup!)10:06:34.532 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}(defn process-data [{:keys [data version]}]
{:result (reduce + data)
:version version})Version 1:
(deref (pocket/cached #'process-data {:data [1 2 3] :version 1}))10:06:34.534 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/process-data
10:06:34.535 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/58/(pocket-book.usage-practices_process-data {:data [1 2 3], :version 1})
{:result 6, :version 1}After changing the function, bump version:
(deref (pocket/cached #'process-data {:data [1 2 3] :version 2}))10:06:34.536 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/process-data
10:06:34.537 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/64/(pocket-book.usage-practices_process-data {:data [1 2 3], :version 2})
{:result 6, :version 2}Both versions coexist in cache β useful for A/B comparison.
Strategy 3: Full Cleanup
For a fresh start, use cleanup! to delete the entire cache:
(pocket/cleanup!)10:06:34.538 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}Testing with Pocket
Tests should be isolated from production caches and from each other. Use binding and cleanup fixtures:
Test Fixture Pattern
(def test-cache-dir "/tmp/my-project-test-cache")
(defn cleanup-fixture [f]
(binding [pocket/*base-cache-dir* test-cache-dir]
(pocket/cleanup!)
(try
(f)
(finally
(pocket/cleanup!)))))
(use-fixtures :each cleanup-fixture)This ensures: 1. Tests use a separate cache directory 2. Cache is cleared before and after each test 3. Tests donβt affect each other
Verifying Cache Behavior
Use an atom to track computation calls:
(pocket/cleanup!)10:06:34.539 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed false}(def call-count (atom 0))(defn tracked-fn [x]
(swap! call-count inc)
(* x x))First call computes:
(reset! call-count 0)0(let [result (deref (pocket/cached #'tracked-fn 5))
calls @call-count]
{:result result :calls calls})10:06:34.541 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/tracked-fn
10:06:34.542 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/4a/(pocket-book.usage-practices_tracked-fn 5)
{:result 25, :calls 1}Second call uses cache (no additional computation):
(let [result (deref (pocket/cached #'tracked-fn 5))
calls @call-count]
{:result result :calls calls}){:result 25, :calls 1}REPL Development Workflow
Inspecting the Cache
Use introspection functions to understand cache state:
(pocket/cleanup!)10:06:34.545 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}(deref (pocket/cached #'transform 1))10:06:34.545 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/transform
10:06:34.546 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/db/(pocket-book.usage-practices_transform 1)
2(deref (pocket/cached #'transform 2))10:06:34.547 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/transform
10:06:34.547 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/e4/(pocket-book.usage-practices_transform 2)
4(deref (pocket/cached #'tracked-fn 3))10:06:34.548 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/tracked-fn
10:06:34.548 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/e8/(pocket-book.usage-practices_tracked-fn 3)
9See all cached entries:
(count (pocket/cache-entries))3Get aggregate statistics:
(:total-entries (pocket/cache-stats))3Visualize cache structure:
(pocket/dir-tree)pocket-dev-practices
βββ db
β βββ (pocket-book.usage-practices_transform 1)
β βββ meta.edn
β βββ value.nippy
βββ e4
β βββ (pocket-book.usage-practices_transform 2)
β βββ meta.edn
β βββ value.nippy
βββ e8
βββ (pocket-book.usage-practices_tracked-fn 3)
βββ meta.edn
βββ value.nippyEach directory contains a meta.edn file with metadata about the cached computation:
(-> (pocket/cache-entries)
first
:path
(str "/meta.edn")
slurp
clojure.edn/read-string){:id "(pocket-book.usage-practices/transform 1)",
:fn-name "pocket-book.usage-practices/transform",
:args-str "[1]",
:created-at "2026-02-09T08:06:34.545970813Z"}This same information is available through the API:
(pocket/cache-entries)[{:path
"/tmp/pocket-dev-practices/db/(pocket-book.usage-practices_transform 1)",
:id "(pocket-book.usage-practices/transform 1)",
:fn-name "pocket-book.usage-practices/transform",
:args-str "[1]",
:created-at "2026-02-09T08:06:34.545970813Z"}
{:path
"/tmp/pocket-dev-practices/e4/(pocket-book.usage-practices_transform 2)",
:id "(pocket-book.usage-practices/transform 2)",
:fn-name "pocket-book.usage-practices/transform",
:args-str "[2]",
:created-at "2026-02-09T08:06:34.547336137Z"}
{:path
"/tmp/pocket-dev-practices/e8/(pocket-book.usage-practices_tracked-fn 3)",
:id "(pocket-book.usage-practices/tracked-fn 3)",
:fn-name "pocket-book.usage-practices/tracked-fn",
:args-str "[3]",
:created-at "2026-02-09T08:06:34.548536434Z"}]Filter entries by function name:
(pocket/cache-entries (str (ns-name *ns*) "/transform"))[{:path
"/tmp/pocket-dev-practices/db/(pocket-book.usage-practices_transform 1)",
:id "(pocket-book.usage-practices/transform 1)",
:fn-name "pocket-book.usage-practices/transform",
:args-str "[1]",
:created-at "2026-02-09T08:06:34.545970813Z"}
{:path
"/tmp/pocket-dev-practices/e4/(pocket-book.usage-practices_transform 2)",
:id "(pocket-book.usage-practices/transform 2)",
:fn-name "pocket-book.usage-practices/transform",
:args-str "[2]",
:created-at "2026-02-09T08:06:34.547336137Z"}]Checking Cached Status
Cached values print their status without forcing computation:
(pocket/cleanup!)10:06:34.557 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}(def pending-value (pocket/cached #'transform 99))Before deref:
(pr-str pending-value)"#<Cached (pocket-book.usage-practices/transform 99) :pending>"(deref pending-value)10:06:34.559 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/transform
10:06:34.560 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/d8/(pocket-book.usage-practices_transform 99)
198After deref:
(pr-str pending-value)"#<Cached (pocket-book.usage-practices/transform 99) :cached>"Debugging with Logging
Enable debug logging to see cache hits, misses, and writes. See the Logging chapter for setup.
With debug logging enabled, youβll see:
Cache miss, computing: ...β computation triggeredCache hit (memory): ...β served from in-memory cacheCache hit (disk): ...β loaded from diskCache write: ...β written to disk
Long Cache Keys
When a cache key string exceeds 240 characters, Pocket falls back to using a SHA-1 hash as the directory name. This ensures the filesystem can handle arbitrarily complex arguments while maintaining correct caching behavior.
(pocket/cleanup!)10:06:34.562 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}(defn process-long-text [text]
(str "Processed: " (count text) " chars"))(def long-text (apply str (repeat 300 "x")))(deref (pocket/cached #'process-long-text long-text))10:06:34.564 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/process-long-text
10:06:34.564 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/6d/6d7a9c06574c78354a8235a2e77b6ed27348e404
"Processed: 300 chars"The entry is stored with a hash-based directory name:
(pocket/dir-tree)pocket-dev-practices
βββ 6d
βββ 6d7a9c06574c78354a8235a2e77b6ed27348e404
βββ meta.edn
βββ value.nippyBut meta.edn inside still contains the full details, so cache-entries and invalidate-fn! work correctly:
(-> (pocket/cache-entries (str (ns-name *ns*) "/process-long-text"))
first
:fn-name)"pocket-book.usage-practices/process-long-text"Serialization Constraints
Pocket uses Nippy for serialization. Most Clojure data structures work, but some donβt:
β Safe to Cache
- Primitive types (numbers, strings, keywords, symbols)
- Collections (vectors, maps, sets, lists)
- Records and most deftypes
- Java Serializable objects
β οΈ Requires Care
- Lazy sequences β Nippy fully realizes them during serialization, which means infinite lazy seqs will hang or OOM. Force lazy seqs with
doallinside your function (see below) to keep realization explicit and catch errors early. The round-trip type may also change (aLazySeqcomes back as a regular seq).
β Cannot Cache
- Open file handles, streams
- Network connections, sockets
- Functions, closures (use vars instead)
- Atoms, refs, agents (stateful references)
Lazy Sequences
Use doall or vec to force evaluation inside your function:
(pocket/cleanup!)10:06:34.569 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}(defn generate-data [n]
;; doall forces full evaluation of the lazy sequence
(doall (range n)))(deref (pocket/cached #'generate-data 5))10:06:34.571 INFO scicloj.pocket.impl.cache - Cache miss, computing: pocket-book.usage-practices/generate-data
10:06:34.572 DEBUG scicloj.pocket.impl.cache - Cache write: /tmp/pocket-dev-practices/79/(pocket-book.usage-practices_generate-data 5)
(0 1 2 3 4)Known Limitations
No automatic cache invalidation β Pocket doesnβt detect when a functionβs implementation changes. Use
invalidate!,invalidate-fn!, or the versioning pattern described above.Requires serializable values β Nippy handles most Clojure types, but you canβt cache functions, atoms, channels, file handles, or other stateful objects.
Disk cache grows indefinitely β The in-memory cache supports eviction policies (LRU, TTL, etc.), but the disk cache has no automatic cleanup. Use
cleanup!orinvalidate-fn!periodically if disk space is a concern.No disk cache TTL β Cached values on disk never expire automatically. If you need time-based expiration, youβll need to manage it externally or use
cleanup!.
Summary
| Practice | Recommendation |
|---|---|
| Function identity | Always use vars (#'fn-name) |
| Invalidation | Manual, versioning, or full cleanup |
| Testing | Use binding + cleanup fixtures |
| Debugging | Enable logging, use introspection |
| Long cache keys | Auto-handled with SHA-1 fallback |
| Serialization | Avoid stateful objects; force lazy seqs with doall |
| Configuration | Use pocket.edn β see Configuration |
(pocket/cleanup!)10:06:34.574 INFO scicloj.pocket - Cache cleanup: /tmp/pocket-dev-practices
{:dir "/tmp/pocket-dev-practices", :existed true}