Dependency Management with Leiningen and deps.edn

How the current Clojure toolchain handles dependencies, aliases, classpaths, and where Leiningen still fits in existing codebases.

Dependency management with Leiningen and deps.edn is one of the places where old Clojure advice goes stale fastest. The current official default is the Clojure CLI with deps.edn. Leiningen is still real, still useful, and still common in established codebases, but it is no longer the best starting assumption for new examples.

That distinction matters because dependency management is not just about downloading jars. It shapes how a team runs code, isolates environments, builds artifacts, debugs classpath problems, and explains the project to the next engineer.

The Current Default: Clojure CLI Plus deps.edn

The Clojure CLI is now the center of the official toolchain. It uses deps.edn for dependency coordinates, paths, aliases, and execution modes. That makes it the simplest default for new work because the mental model is direct:

  • :deps declares libraries
  • :paths declares source roots
  • :aliases describe alternate classpaths and execution contexts
  • CLI modes such as -M, -X, and -T decide how code is run

A minimal project can stay very small:

1{:paths ["src"]
2 :deps {org.clojure/clojure {:mvn/version "1.12.0"}}
3 :aliases
4 {:dev  {:extra-paths ["dev"]}
5  :test {:extra-paths ["test"]}}}

That simplicity is the real advantage. You can explain the project layout, execution flow, and dependency graph without first teaching a large build abstraction.

What Leiningen Still Does Well

Leiningen is not obsolete. It remains common in mature projects, published libraries, and teams that already have a working project.clj ecosystem. It still gives you a cohesive model for:

  • dependency declaration
  • profiles
  • REPL tasks
  • packaging flows
  • plugin-based project automation

An established Leiningen application can still be a perfectly healthy codebase:

1(defproject my-app "0.1.0-SNAPSHOT"
2  :dependencies [[org.clojure/clojure "1.12.0"]]
3  :profiles {:dev {:resource-paths ["dev-resources"]}
4             :test {:test-paths ["test"]}})

The practical rule is:

  • use deps.edn by default for new examples and new projects
  • keep Leiningen when it already serves a stable existing system well

That is a better rule than pretending one tool has erased the other.

Dependency Declaration Is Only Half The Story

New Clojure users often think deps.edn is just a different syntax for listing dependencies. It is more than that. It is also the basis for how the CLI computes classpaths and execution contexts.

That means aliases are central:

1{:paths ["src"]
2 :deps {org.clojure/clojure {:mvn/version "1.12.0"}}
3 :aliases
4 {:dev  {:extra-paths ["dev"]}
5  :test {:extra-paths ["test"]}
6  :build {:ns-default build}}}

With the current CLI, you then choose execution style explicitly:

  • clj for a REPL
  • clojure -M to run a main namespace or script
  • clojure -X to call a function with keyword arguments
  • clojure -T to run an installed or aliased tool

That explicitness is one of the biggest differences from older tutorials.

How To Inspect Dependency Problems

The most dangerous dependency bug is not “a library failed to download.” It is “the classpath resolved, but not the way you thought.”

So the real skill is inspection:

  • use clojure -X:deps tree to inspect the dependency tree in current CLI workflows
  • know that older tutorials often show clj -Stree, which still exists in many environments
  • inspect aliases and classpaths before guessing
  • treat exclusions and overrides as deliberate design choices, not random fixes

For Leiningen, lein deps :tree still plays the same diagnostic role.

The core lesson is the same in both worlds: when versions clash, inspect the actual graph before “fixing” anything.

Another source of stale guidance is mixing dependency declaration and artifact-building into one mental bucket. In modern Clojure, they are related but distinct:

  • deps.edn is about dependencies, classpaths, paths, and aliases
  • tools.build is about scripted artifact creation and release tasks

That separation is healthy. It keeps the dependency model simple while still allowing explicit build logic when you need jars, uberjars, generated poms, or release automation.

If a tutorial teaches deps.edn as though it should replace every build concern automatically, it is teaching the wrong abstraction boundary.

Choosing Between The Two

Use deps.edn when:

  • you are starting a new service, library, or internal tool
  • you want the official current workflow
  • you want explicit execution modes with -M, -X, and -T
  • you want a smaller mental model for classpaths and dependencies

Stay with Leiningen when:

  • the existing system is already stable and understood
  • the project depends on established plugin workflows
  • migration would create churn without meaningful engineering value
  • the team’s operational knowledge is already built around project.clj

This is an engineering decision, not a purity test.

A Better Mental Model

    flowchart LR
	    A["Dependency Coordinates"] --> B["deps.edn or project.clj"]
	    B --> C["Classpath Resolution"]
	    C --> D["Execution Mode or Build Task"]
	    D --> E["REPL, App Run, Tool Run, or Artifact Build"]

The important thing to notice is that dependency management is upstream of almost everything else. If the classpath model is muddy, the rest of the project feels muddy too.

Common Anti-Patterns

  • teaching Leiningen as the universal default for all new Clojure work
  • assuming deps.edn is only a prettier dependency file
  • using exclusions and overrides without first inspecting the graph
  • mixing build-automation questions into every dependency conversation
  • migrating a healthy Leiningen codebase for fashion rather than operational value

Key Takeaways

  • The official current default is the Clojure CLI with deps.edn.
  • Leiningen remains a valid tool in existing systems.
  • Dependency management is really about classpaths and execution flow, not only library download.
  • Use tree-inspection commands before making version overrides or exclusions.
  • Treat tools.build as a complementary build tool, not a replacement for dependency declaration.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026