Clojure
Clojure is a dynamic, functional programming language in the Lisp family that runs on the Java Virtual Machine. Created by Rich Hickey in 2007, it combines three ideas: immutable data by default, simple data structures — maps, vectors, sets — as the primary way to model information, and interactive development through a REPL connected to the running program. Because it compiles to JVM bytecode, Clojure uses Java libraries directly and deploys anywhere Java runs. ClojureScript brings the same language to JavaScript environments, and Babashka makes it a fast scripting language.
Clojure is used in production for financial systems, data processing, and web services — Nubank, one of the world's largest digital banks, runs much of its platform on it. Its community is smaller than Java's or Python's, but its design has influenced many mainstream languages' approaches to immutability and data.
TL;DR
- Clojure is a Lisp: code is written as data (lists), which enables powerful macros.
- Data is immutable; "changes" create new versions efficiently using persistent data structures.
- Programs are built from plain data (maps, vectors, keywords) and pure functions that transform it.
- REPL-driven development means evaluating code inside the running application from your editor.
- Shared state uses explicit reference types — atoms, refs, agents — plus core.async channels.
- Java interop gives access to the entire JVM ecosystem; the Clojure CLI with
deps.ednmanages dependencies.
Quick Example
Revenue per customer from a collection of orders — data in, data out:
Managing shared state with an atom, which applies updates atomically and retries on contention:
The threading macro ->> passes each result as the last argument to the next function, so pipelines read top to bottom.
Core Concepts
Syntax and Homoiconicity
Clojure code is made of the same data structures programs manipulate: (f a b) is a list whose first element is the function. Because code is data, macros can transform code before it is compiled, letting libraries add new control structures — ->>, when-let, and core.async's go blocks are all macros. Macros are powerful but should be a last resort after functions.
Immutable Persistent Data Structures
Vectors, maps, and sets never change in place. Functions such as assoc, conj, and update return new versions that share structure with the old ones, making "copies" cheap. Immutability removes whole categories of bugs — shared mutable state, defensive copying, unexpected aliasing — and makes concurrent code far simpler.
Data-Oriented Design
Instead of classes with methods, Clojure programs typically represent entities as maps with namespaced keywords (:order/id, :order/total) and write generic functions over them. Data can be validated and described with clojure.spec or Malli, which also generate test data and documentation.
REPL-Driven Development
Developers connect their editor — CIDER for Emacs, Calva for VS Code, Cursive for IntelliJ — to a running REPL and evaluate functions as they write them, inspecting real data at each step. Redefining a function updates the running program without a restart. Tests still matter, but much exploration happens interactively.
Concurrency
Clojure 1.12 and modern JVMs also work well with Java's virtual threads for blocking I/O. See Java Virtual Threads.
Tooling and Ecosystem
A project's dependencies and aliases live in deps.edn, used by the official Clojure CLI:
Applications usually deploy as an uberjar — a single JAR with all dependencies — run with java -jar, often in a container.
When to Choose Clojure
Best Practices
Model With Plain Data
Prefer maps and vectors with namespaced keywords over custom types. Generic functions, serialization, and debugging all become easier.
Keep Side Effects at the Edges
Write a pure core that transforms data and push database calls, HTTP, and logging to thin outer layers. Pure functions are easy to test at the REPL and in unit tests.
Validate at Boundaries
Use spec or Malli to validate incoming data from HTTP requests, messages, and configuration, rather than scattering checks through the code.
Add Type Hints Where Performance Matters
Reflection on Java interop calls is slow. Enable *warn-on-reflection* and add type hints in hot paths.
Common Mistakes
Writing Java in Clojure
Laziness Surprises
Macros Where Functions Would Do
FAQ
What is Clojure used for?
Backend services, data processing, financial and business-rule-heavy systems, and web applications with ClojureScript frontends. Its strengths are working with complex data, concurrency, and interactive development.
Is Clojure a functional language?
Yes. It emphasizes immutable data and pure functions, while allowing controlled side effects and mutable state through explicit reference types and Java interop.
Does Clojure run on the JVM?
The main implementation compiles to JVM bytecode and interoperates directly with Java libraries. ClojureScript targets JavaScript, ClojureCLR targets .NET, and Babashka provides a native scripting runtime.
Is Clojure hard to learn?
The syntax is small and regular, but parenthesized prefix notation, immutability, and functional design take adjustment for developers used to object-oriented languages. Many find the REPL makes learning fast once set up.
Clojure or Scala?
Both run on the JVM with functional features. Scala is statically typed with a rich type system and object-oriented features; Clojure is dynamic, Lisp-based, and data-oriented. See Scala.
Related Topics
- Programming Languages — Comparing language families
- Java — The platform Clojure runs on
- Scala — Another functional JVM language
- Elixir — Functional programming on the BEAM
- Concurrency Patterns — Approaches to shared state
- JVM Performance Tuning — Running Clojure efficiently