Package a small Clojure application as an executable uberjar, understand when :gen-class and AOT compilation matter, and verify the artifact with java -jar.
Packaging is where Clojure looks most familiar to Java engineers: the final artifact is often still a JAR, and production still runs on a JVM. The Clojure-specific part is deciding when you need generated classes, ahead-of-time compilation, and an all-dependencies uberjar.
An uberjar is a deployable JAR that contains your application code plus the libraries it needs at runtime.
| Packaging concern | Java mental model | Clojure detail |
|---|---|---|
| Entry point | Main-Class in a manifest |
Namespace with -main and generated class support |
| Dependencies | Maven Shade, Gradle Shadow, Spring Boot jar | Dependencies bundled into a standalone jar |
| Runtime | java -jar app.jar |
Same JVM command |
| Build output | target/ or build/ |
Usually target/ for Leiningen |
Use an uberjar when you want a simple JVM deployment unit. Do not use it as a substitute for proper environment configuration, logging, health checks, or dependency security review.
For a first packaged app, Leiningen is the shortest path because lein uberjar is built into the tool.
Create a project:
1lein new app hello-world
2cd hello-world
Check project.clj:
1(defproject hello-world "0.1.0-SNAPSHOT"
2 :description "Small packaged Clojure app"
3 :dependencies [[org.clojure/clojure "1.12.4"]]
4 :main ^:skip-aot hello-world.core
5 :target-path "target/%s"
6 :profiles {:uberjar {:aot :all}})
The important fields are:
| Field | Why it matters |
|---|---|
:dependencies |
Adds Clojure and library dependencies to the build |
:main |
Points to the namespace containing -main |
^:skip-aot |
Avoids AOT during normal development |
:profiles {:uberjar ...} |
Enables AOT for the packaged artifact |
:target-path |
Keeps generated files under target/ |
Open src/hello_world/core.clj:
1(ns hello-world.core
2 (:gen-class))
3
4(defn greeting [name]
5 (str "Hello, " name "!"))
6
7(defn -main
8 [& args]
9 (println (greeting (or (first args) "Clojure"))))
The (:gen-class) directive is the key difference from the simple CLI run path. It tells Clojure to generate a JVM class that can act as the Java-visible entry point.
Without :gen-class |
With :gen-class |
|---|---|
Fine for clojure -M -m hello-world.core |
Needed for a Java-executable main class |
| No generated class required | Generated class can be referenced by the jar manifest |
| Best for REPL and script-like execution | Best for java -jar packaging |
Run tests before packaging:
1lein test
Build the uberjar:
1lein uberjar
Find the standalone artifact:
1find target -maxdepth 3 -name '*standalone.jar'
Run it:
1java -jar target/uberjar/hello-world-0.1.0-SNAPSHOT-standalone.jar Ada
Expected output:
1Hello, Ada!
If your exact target path differs, use the path printed by the find command.
flowchart LR
A["Source namespaces"] --> B["lein test"]
B --> C["lein uberjar"]
C --> D["Standalone JAR"]
D --> E["java -jar"]
This is the same deployment shape Java engineers already know, but Clojure adds one extra design question: whether the code should stay dynamically loaded for development and only be AOT-compiled for packaging.
If the project is built around deps.edn, do not add Leiningen just for packaging unless the team has chosen that migration. The Clojure CLI ecosystem usually packages with a build script, commonly using tools.build.
| Project type | Reasonable packaging path |
|---|---|
| Existing Leiningen app | lein uberjar |
Existing deps.edn app |
tools.build or team build script |
| Library | Publish a library jar, not an application uberjar |
| Containerized service | Build jar or classpath artifact, then put runtime config in the container |
The key principle is consistency: use the build tool that already owns the project unless there is a deliberate migration plan.
| Symptom | Likely cause | Fix |
|---|---|---|
Could not find or load main class |
Missing :main, missing :gen-class, or no AOT for main namespace |
Check project.clj and namespace declaration |
Works with lein run but not java -jar |
Development classpath differs from packaged jar | Inspect dependencies and resources in the jar |
| Resource file missing at runtime | resources/ not included or path is wrong |
Put runtime files under resources/ and load from classpath |
| Slow or surprising build | AOT compiling more namespaces than expected | Keep AOT scoped to packaging profile |
| Environment-specific config bundled into jar | Config treated as code artifact | Move secrets and deployment config outside the jar |
lein uberjar.(:gen-class) and AOT compilation matter when java -jar needs a generated main class.java -jar, not only with lein run.