Generate a Leiningen application, read its project.clj and namespace layout, run the app, run tests, and start the project REPL without confusing the scaffold with architecture.
The fastest way to understand Leiningen is to create a small app and run each command once. Treat the scaffold as a learning object, not as the architecture you must use forever.
Run:
1lein new app orders-demo
2cd orders-demo
The app template creates an application with a -main entry point. Without app, Leiningen creates a more library-oriented skeleton.
You should see a structure like this:
1orders-demo/
2├── project.clj
3├── README.md
4├── resources/
5├── src/
6│ └── orders_demo/
7│ └── core.clj
8└── test/
9 └── orders_demo/
10 └── core_test.clj
| File or directory | Why it matters |
|---|---|
project.clj |
Owns dependencies, project metadata, main namespace, profiles, and plugins |
src/orders_demo/core.clj |
Production namespace generated from the project name |
test/orders_demo/core_test.clj |
Test namespace matching the source namespace |
resources/ |
Classpath resources such as config templates or logging files |
target/ |
Generated output after tests, compilation, or packaging |
Notice the dash-to-underscore mapping:
| Name | Form |
|---|---|
| Project name | orders-demo |
| Namespace | orders-demo.core |
| Directory path | orders_demo/core.clj |
project.cljA generated app usually resembles:
1(defproject orders-demo "0.1.0-SNAPSHOT"
2 :description "FIXME: write description"
3 :dependencies [[org.clojure/clojure "1.12.5"]]
4 :main ^:skip-aot orders-demo.core
5 :target-path "target/%s"
6 :profiles {:uberjar {:aot :all}})
For a Java engineer, the useful reading order is:
Do not edit every placeholder immediately. First prove that the generated project runs.
Open src/orders_demo/core.clj and keep the side effect at the edge:
1(ns orders-demo.core
2 (:gen-class))
3
4(defn summarize-order [order]
5 (str "Order " (:id order) " has " (:line-count order) " lines"))
6
7(defn -main
8 [& args]
9 (println
10 (summarize-order {:id (or (first args) "demo")
11 :line-count 3})))
This tiny design matters:
| Function | Role |
|---|---|
summarize-order |
Pure behavior you can call from tests or REPL |
-main |
Runtime boundary that handles command-line input and printing |
From the project root:
1lein run 1001
Expected output:
1Order 1001 has 3 lines
If dependencies are not downloaded yet, Leiningen will resolve them as part of the task. That is why Java developers do not usually need a separate “install dependencies first” step for ordinary local work.
Open test/orders_demo/core_test.clj and test the pure function:
1(ns orders-demo.core-test
2 (:require [clojure.test :refer [deftest is testing]]
3 [orders-demo.core :as core]))
4
5(deftest summarize-order-test
6 (testing "summarizes an order"
7 (is (= "Order 1001 has 3 lines"
8 (core/summarize-order {:id "1001" :line-count 3})))))
Run:
1lein test
The goal is to establish the same basic loop you expect in Java: edit code, run tests, keep the boundary thin.
Run:
1lein repl
Inside the REPL:
1(require '[orders-demo.core :as core])
2(core/summarize-order {:id "repl" :line-count 2})
This is the Clojure inner loop. You are not only proving that main runs; you are loading the project and calling functions directly.
Once run and test commands are clean:
1lein uberjar
Then verify the generated standalone jar with java -jar. Packaging should confirm a working app, not compensate for an unclear project structure.
lein new app gives you a small runnable app, not a full architecture.project.clj before changing it.-main small.lein test and lein repl early, not only lein run.lein uberjar only after the local loop works.