Configuration Without the Ceremony

Almost every program needs configuration: database connection strings, log file paths, data locations. The difficulty is that configuration is dynamic—it arrives at runtime, not compile time—and implicit, because it influences functions without being an explicit argument. Two common strategies have evolved to handle this, each with notable trade-offs.

The Global Approach

Global variables are the simplest way to make configuration implicitly available throughout a codebase. In object-oriented languages, a class-level singleton or eigenclass works just as well:

module App
  API_SERVER = "api3"
end

def save(record)
  http_put(APP::API_SERVER, record)
end

Erlang applications frequently use a globally-named module for the same purpose:

class App
  def self.config; @config; end
end

App.config.api_server = "api3"

App.config.api_server

Globals are concise and every thread observes the same values—all code everywhere sees them. However, they fail when you need library isolation, running concurrent copies of an application, or writing tests that exercise the same functions under multiple configurations.

Object Graph Traversal

A more disciplined OOP approach puts configuration in instances. The application constructs a graph of objects, each carrying the configuration it needs:

{ok, Server} = app_config:get(api_server),

If the APIClient needs the logger, it can hold a reference to the application and traverse the graph to find it:

class App
  def initialize(config)
    @api_client = App::APIClient config[:api_server]
    @logger = Logger.new config[:logger]
  end
end

This is essentially passing configuration through constructors, with the bonus of centralized lookups for local services. Thread-safety improves: you can run multiple applications concurrently without them interfering. The downsides are significant traversal overhead and testing friction—you must stand up all dependencies before you can instantiate a single object, and there is no way to access these instance values from class methods.

What We Actually Want

Stepping back, the goal is to refactor functions like this:

class APIClient
  def initialize(app, config)
    @app = app
    @server = config[:server]
  end

  def get
    @app.logger.log "getting"
  end
end

…into something like this, where the configuration is implicit:

f(config, x) = g(config, x * 2)
g(config, y) = h(config, y + 1)
h(config, z) = config + z

This is the notion of dynamic scope: variables bound for every function in the call stack without being explicit parameters. Two properties make this useful:

  1. The binding applies only within and below the binding expression; when control returns, the variable reverts to its prior value.
  2. The binding is per-thread and inherited by child threads, enabling two parallel invocations to use different configurations.

Scala offers implicit parameters, though they do not propagate across threads, which complicates deferred work like futures. InheritableThreadLocal in Java provides thread isolation if you carefully clean up the binding context. Scala's DynamicVariable wraps this as a mutable, thread-local, thread-inherited object bound only during closure execution. Since Scala lacks native dynamic scope, the variable is accessed statically:

f(x) = g(x*2)
g(y) = h(y+1)
h(z) = config + z

Calling config.value() to fetch the binding is a minor wart, but the semantics are correct and the code stays clean without extra bookkeeping.

Native Dynamic Scope

Languages with built-in dynamic scope—most Lisps, Perl, and some Haskell extensions—express this directly:

class App {
  def start() {
    App.config.withValue(someConfigStructure) {
      httpServer.run();
    }
  }
}

object App {
  val config = new DynamicVariable[MyConfig];
}

class HttpServer {
  def run() {
    listen(App.config.value.httpPort)
  }
}

The objection to dynamic scope is name capture: a deep function might accidentally use the same variable name. Clojure sidesteps this with namespaces; other code is unaffected unless it explicitly includes the namespace.

In Clojure, dynamic vars have a root value shared across threads and an overrideable thread-local value. Yet not all closures capture dynamic scope: (Thread. (fn [] …)) runs with fresh root bindings, while future, bound-fn, and similar constructs preserve the caller's dynamic frames.

Thread-Inheritable Vars in Clojure

Adopting Scala's pattern, you can define a reference backed by an InheritableThreadLocal that also implements IDeref, Clojure's dereference interface for vars, refs, atoms, and agents:

(ns app.config)
(def ^:dynamic config nil)

(ns app.core)
(defn start []
  (binding [app.config/config some-config-structure]
    (http-server/run)))

(ns app.http-server
  (:use app.config))
(defn run []
  (listen (:http-port config)))

A macro sets the binding within a scope:

(defn thread-inheritable
  "Creates a dynamic, thread-local, thread-inheritable object, with initial
  value 'value'. Set with (.set x value), read with (deref x)."
  [value]
  (doto (proxy [InheritableThreadLocal IDeref] []
          (deref [] (.get this)))
    (.set value)))

With this in place, you can define and dynamically rebind a config var:

(defn- set-dynamic-thread-vars!
  "Takes a map of vars to values, and assigns each."
  [bindings-map]
  (doseq [[v value] bindings-map]
    (.set v value)))

(defmacro inheritable-binding 
  "Creates new bindings for the (already-existing) dynamic thread-inherited
  vars, with the supplied initial values. Executes exprs in an implict do, then
  re-establishes the bindings that existed before. Bindings are made
  sequentially, like let."
  [bindings & body]
  `(let [inner-bindings# (hash-map ~@bindings)
         outer-bindings# (into {} (for [[k# v#] inner-bindings#]
                                        [k# (deref k#)]))]
    (try
      (set-dynamic-thread-vars! inner-bindings#)
       ~@body
       (finally
         (set-dynamic-thread-vars! outer-bindings#)))))

A realistic example:

(def config (thread-inheritable :default))

(prn "Initially" @config)
(inheritable-binding [config :inside]
  ; In any functions we call, (deref config) will be :inside.
  (prn "Inside" @config)
  
  ; We can safely evaluate multiple bindings in parallel. It's the
  ; many-worlds hypothesis in action!
  (inheritable-binding [config :future]
    (future (prn "Future" @config)))
  
  ; Unlike regular ^:dynamic vars, bindings are inherited in child threads.
  (inheritable-binding [config :thread]
    (Thread. (fn [] (prn "In unbound thread" @config)))))

This yields mutable, thread-safe, thread-inherited implicit variables. Note that these are not dynamic bindings, so bound-fn will not capture them. Use ^:dynamic with bound-fn when passing closures between existing threads; use this binding approach when inheritance of thread values is required.

(defmacro with-config 
  [m & body]
  `(inheritable-binding [config ~m] ~@body))

(defn start-server []
  (listen (:port @config)))

(with-config {:port 2}
  (start-server))