A Clojure/ClojureScript library for scoped values. On JDK 25+, uses Java's ScopedValue API for efficient context
propagation with virtual threads. On older JDKs, falls back to ThreadLocal. In ClojureScript, falls back to binding.
Clojure:
ThreadLocal)ScopedValue)On JDK 25+, the library uses Java's ScopedValue API for maximum performance. On older JDKs, it
automatically falls back to a ThreadLocal-based implementation with identical semantics.
ClojureScript:
binding);; deps.edn
co.multiply/scoped {:mvn/version "0.1.12"}
This library emerged while working on async code where it became clear that extracting and setting thread bindings
accounted for the vast majority of the overhead involved. While I can't make broad claims about the efficiency of
ScopedValue (introduced in Java 21, GA in Java 25), it made a significant difference for my own code. Switching to
scoped values cut about 95% of the overhead: from ~20μs to ~1μs per async operation.
This library provides a way to use ScopedValue when running on JDK 25, while providing a semantically identical
fallback to ThreadLocal when running on older versions of the JDK. It also provides some basic support for
ClojureScript, to ease use in CLJC contexts.
scoping (CLJ + CLJS)Establish scoped bindings, similar to binding:
(require '[co.multiply.scoped :refer [scoping ask]])
(def ^:dynamic *user-id* nil)
(def ^:dynamic *request-id* nil)
(scoping [*user-id* 123
*request-id* "abc"]
(ask *user-id*))
;; => 123
Scopes nest naturally; inner bindings shadow outer ones:
(scoping [*user-id* 1]
(scoping [*user-id* 2]
(ask *user-id*)))
;; => 2
ask (CLJ + CLJS)Access a scoped value. Falls back to the var's root binding if not in scope:
(require '[co.multiply.scoped :refer [ask scoping]])
(def ^:dynamic *user-id* :default)
(ask *user-id*) ; => :default
(scoping [*user-id* 123]
(ask *user-id*))
;; => 123
Throws IllegalStateException if the var is unbound and not in scope.
Gotcha: It can be easy to forget ask and reference the var directly. With a default value, this fails silently:
(def ^:dynamic *user-id* :default)
(scoping [*user-id* 123]
(str "User: " *user-id*)) ; Oops, forgot `ask`
;; => "User: :default" (wrong!)
Prefer unbound vars. They're more likely to fail when used, making the mistake obvious:
(def ^:dynamic *user-id*)
(scoping [*user-id* 123]
(+ *user-id* 1)) ; Forgot `ask`
;; => ClassCastException: Var$Unbound cannot be cast to Number
current-scope (CLJ only)Capture the current scope map for later restoration:
(require '[co.multiply.scoped :refer [current-scope scoping]])
(def ^:dynamic *user-id*)
(scoping [*user-id* 123]
(current-scope))
;; => {#'*user-id* 123}
assoc-scope (CLJ only)Extend a captured scope with additional bindings without creating another lambda:
(require '[co.multiply.scoped :refer [assoc-scope current-scope scoping]])
(def ^:dynamic *user-id*)
(def ^:dynamic *request-id*)
(def captured
(scoping [*user-id* 123]
(current-scope)))
(assoc-scope captured *request-id* "abc")
;; => {#'*user-id* 123, #'*request-id* "abc"}
This is useful when you have a captured scope and want to add bindings before restoring it, avoiding the overhead of
nesting scoping inside with-scope.
with-scope (CLJ only)Restore a previously captured scope:
(require '[co.multiply.scoped :refer [ask current-scope scoping with-scope]])
(def ^:dynamic *user-id*)
(def captured
(scoping [*user-id* 123]
(current-scope)))
(with-scope captured
(ask *user-id*))
;; => 123
Combined with assoc-scope:
(with-scope (assoc-scope captured *request-id* "abc")
(ask *request-id*))
;; => "abc"
Capture and restore scope across virtual thread boundaries:
(require '[co.multiply.scoped :refer [ask current-scope scoping with-scope]])
(defmacro vt
[& body]
`(let [scope# (current-scope)]
(Thread/startVirtualThread
(fn run# [] (with-scope scope# ~@body)))))
(def ^:dynamic *user-id*)
(scoping [*user-id* 123]
(vt (println "User:" (ask *user-id*))))
;; Prints: "User: 123"
(scoping [*user-id* 123]
;; Nested virtual threads
(vt (vt (println "User:" (ask *user-id*)))))
;; Prints: "User: 123"
Eclipse Public License 2.0. Copyright (c) 2025 Multiply. See LICENSE.
Authored by @eneroth
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 |