Clojure Internals, History and Best Practices
46.1 How Clojure Works
Every Clojure form is compiled to bytecode (even at the REPL). There's no interpreter on the JVM.
Key interfaces: IFn, ISeq, IPersistentCollection, Associative, ILookup, IDeref, IReduce. Protocols compile to JVM interfaces plus a dispatch cache.
Source → Reader (text → data) → Macroexpansion → Analyzer → Compiler → JVM bytecode (classes per fn) → JIT
Persistent collections: HAMTs (hash array mapped tries) for maps/sets, 32-way tries with a tail for vectors
Vars: namespaced mutable references with thread-local binding; fns are compiled classes implementing IFn46.2 History
Rich Hickey develops Clojure; public release in October 2007 Datomic released; reducers (1.5)
Transducers (1.7, released 2015); reader conditionals clojure.spec (1.9, released 2017)
Clojure CLI and deps.edn become official; Clojure 1.10 (2018)
Nubank acquires Cognitect (Clojure's steward) Clojure 1.11 (keyword-arg maps, parse-long, update-vals, iteration)
Clojure 1.12 (qualified methods as values, add-lib, functional interface conversion, array class syntax)
46.3 Best Practices
✅ Model with plain data (maps, vectors, sets) and namespaced keywords. Keep functions small and pure.
✅ Push side effects to the edges (a functional core and an imperative shell).
✅ Use one atom per application state (often a map); use refs only for coordinated multi-identity updates.
✅ Develop at the REPL with rich comment blocks; reload with tools.namespace/integrant-repl.
✅ Validate at boundaries with Malli or spec. Use ex-info with data for errors.
✅ Lint with clj-kondo, format with cljfmt/zprint, and test with clojure.test + test.check.
✅ Prefer threading macros for readable pipelines and destructuring for clear parameters.
✅ Enable reflection warnings in performance-sensitive code.
❌ Side effects inside lazy sequences, swap! functions, or dosync
❌ Deeply nested data access without get-in/update-in/Specter ❌ Macros when a function would do
❌ Holding the head of large lazy sequences ❌ Using read-string on untrusted input (use clojure.edn/read-string)
❌ Overusing records and protocols where plain maps and functions suffice