Two Build Tooling Paths
Clojure has two mainstream tools for managing dependencies, the classpath, and common project tasks: Leiningen, the older and still widely used batteries-included tool configured via project.clj, and the official tools.deps CLI configured via deps.edn. Both resolve library versions from Maven-style coordinates and build a classpath, but they differ sharply in philosophy — one bundles conventions and a large plugin ecosystem, the other stays deliberately minimal and composable.
Cricket analogy: Choosing between Leiningen and the deps.edn tools.deps CLI is like a team choosing between a traditional, everything-included tour manager versus building your own support staff a la carte, composing exactly the pieces you need, nothing more.
Leiningen: project.clj Anatomy
A Leiningen project centers on defproject, which declares the project's name, version, and dependencies in one map-like form. :dependencies lists required libraries, :plugins lists build-time tooling like lein-ring, and :profiles layers extra configuration onto specific contexts — most commonly :dev for development-only dependencies and :uberjar for producing a standalone runnable JAR via lein uberjar.
Cricket analogy: A defproject form is like a team's official registration document naming the squad, the support staff hired for the tour, and separate training regimes for practice matches versus the actual final, all filed before lein run kicks off the innings.
deps.edn and tools.deps
deps.edn declares :paths (source and resource directories on the classpath), :deps (a map of library coordinates to versions), and :aliases, which are named, opt-in configurations for extra paths, dependencies, or main options. Nothing in an alias applies unless invoked explicitly — clj -M:alias runs a namespace's -main function, while clj -X:alias invokes a specific function directly, passing trailing key/value pairs as an EDN argument map.
Cricket analogy: deps.edn's :paths is like specifying exactly which nets and pitches the team practices on, :deps lists the specific sparring partners brought in, and :aliases are named custom drills, invoked only when called by name on the command line.
;; project.clj (Leiningen)
(defproject myapp "0.1.0-SNAPSHOT"
:description "Example service"
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.12.0"]
[cheshire "5.12.0" :exclusions [org.clojure/clojure]]]
:plugins [[lein-ring "0.12.6"]]
:profiles {:dev {:dependencies [[ring/ring-devel "1.12.0"]]}
:uberjar {:aot :all}}
:main ^:skip-aot myapp.core)
;; deps.edn (tools.deps)
{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.11.1"}
ring/ring-core {:mvn/version "1.12.0"}}
:aliases
{:dev {:extra-paths ["dev"]
:extra-deps {ring/ring-devel {:mvn/version "1.12.0"}
nrepl/nrepl {:mvn/version "1.1.0"}}}
:test {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]}}}
Rule of thumb: reach for Leiningen when you want batteries-included conventions (uberjar packaging, a huge plugin ecosystem, one config format for everything). Reach for deps.edn/tools.deps when you want a lighter, official, more composable setup — especially for libraries, or when you want fine-grained control via aliases without a plugin layer in between.
Managing Dependencies and Profiles/Aliases
Both tools resolve a full transitive dependency graph and must settle conflicts when two libraries pull in different versions of the same underlying artifact. :exclusions lets you explicitly drop an unwanted transitive dependency so a chosen version wins deterministically. lein deps :tree and clj -Stree both print the resolved graph, which is the first thing to check when a classpath issue or unexpected class shows up at runtime.
Cricket analogy: Two selectors independently recommending the same all-rounder for different reasons is fine, but two different scouting reports disagreeing on the same player's exact fitness rating forces the selection committee to pick one authoritative source, just as :exclusions removes a duplicate transitive dependency pulling in a conflicting version.
Transitive dependency conflicts are one of the most common build headaches in both tools. Run lein deps :tree (Leiningen) or clj -Stree (tools.deps) to see the resolved dependency graph, and use :exclusions to drop an unwanted transitive version before it silently shadows the one you actually need.
- Leiningen (project.clj) and tools.deps (deps.edn) are the two mainstream Clojure build tools; both manage dependencies and the classpath.
- defproject in project.clj declares :dependencies, :plugins, and environment-specific :profiles like :dev and :uberjar.
- deps.edn declares :paths (source directories), :deps (libraries), and :aliases (named, explicitly-invoked task configurations).
- clj -M:alias runs a main namespace under an alias's extra deps/paths; clj -X:alias invokes a specific function with an EDN argument map.
- Leiningen is batteries-included with a large plugin ecosystem; tools.deps is the leaner, official, more composable option favored for libraries.
- Use :exclusions to drop an unwanted transitive dependency version when two libraries pull in conflicting versions of the same artifact.
- lein deps :tree and clj -Stree both print the resolved dependency graph to help debug version conflicts.
Practice what you learned
1. In project.clj, what does the :profiles map typically configure?
2. What is the purpose of :aliases in deps.edn?
3. What's the key difference between clj -M and clj -X?
4. Why would you add an :exclusions entry to a dependency?
5. Which command shows the resolved dependency tree to help debug a version conflict?
Was this page helpful?
You May Also Like
Namespaces and require
Learn how Clojure organizes code into namespaces and how ns, require, import, and refer control how symbols are loaded and referenced across files.
Testing with clojure.test
Learn Clojure's built-in testing framework — deftest, is, are, fixtures, and how to organize and selectively run tests in a real project.
Clojure and Java Interop
Understand how Clojure calls Java classes and methods directly via dot notation, implements interfaces with proxy/reify/gen-class, and handles exceptions and type hints for performance.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics