Browse Learn Clojure Foundations as a Java Developer

Package a Clojure App as an Uberjar

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.

What an Uberjar Does

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.

Use Leiningen for the First Uberjar

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/

Ensure the Namespace Can Generate a Class

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

Build and Verify

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.

Packaging Flow

    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.

Clojure CLI Projects

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.

Troubleshooting Packaging

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

Key Takeaways

  • An uberjar is a JVM deployment artifact, not a Clojure-only concept.
  • Leiningen provides the shortest first path with lein uberjar.
  • (:gen-class) and AOT compilation matter when java -jar needs a generated main class.
  • Verify the packaged artifact with java -jar, not only with lein run.
  • Keep secrets and environment configuration outside the packaged jar.

Quiz: Package App

### What is the main purpose of an uberjar? - [x] To package application code and runtime dependencies into one executable JAR - [ ] To run only unit tests - [ ] To replace the JVM - [ ] To store source files without dependencies > **Explanation:** An uberjar is a standalone deployment artifact that can be run with `java -jar`. ### Why does a packaged Clojure app often use `(:gen-class)`? - [x] It generates a Java-visible class for the executable entry point. - [ ] It changes Clojure syntax into Java syntax. - [ ] It installs Leiningen automatically. - [ ] It disables dependency resolution. > **Explanation:** `:gen-class` lets Clojure generate a class that the JVM can use as the jar entry point. ### What should you run after building the uberjar? - [x] `java -jar` against the generated standalone jar - [ ] Only `lein repl` - [ ] Only `clojure -Spath` - [ ] `git reset --hard` > **Explanation:** Packaging is not verified until the built artifact runs the same way production will run it.
Revised on Saturday, May 23, 2026