Map Clojure source paths, namespaces, tests, resources, and build files to the Java project concepts you already know so a small project stays easy to navigate.
Clojure project structure is lighter than a typical Java application, but it is not structureless. The important difference is that Clojure organizes runtime code around namespaces and classpath roots rather than classes under package directories.
A small Clojure CLI project usually starts like this:
1hello-world/
2├── deps.edn
3├── resources/
4│ └── logback.xml
5├── src/
6│ └── hello_world/
7│ └── core.clj
8└── test/
9 └── hello_world/
10 └── core_test.clj
| Path | Java analogy | Clojure role |
|---|---|---|
deps.edn |
pom.xml or build.gradle |
Declares paths, dependencies, aliases, and command options |
src/ |
src/main/java |
Classpath root for production namespaces |
test/ |
src/test/java |
Classpath root for test namespaces |
resources/ |
src/main/resources |
Runtime resources placed on the classpath |
target/ |
target/ or build/ |
Generated artifacts, class files, jars, and build output |
Most Clojure source files declare one namespace at the top:
1(ns hello-world.core)
That namespace belongs in:
1src/hello_world/core.clj
The mapping is mechanical:
| Namespace part | File-system result |
|---|---|
Dot, as in hello-world.core |
Directory boundary |
Hyphen, as in hello-world |
Underscore in the file path |
Final segment, as in core |
File name with .clj |
flowchart LR
A["Namespace hello-world.core"] --> B["Classpath root src/"]
B --> C["Directory hello_world/"]
C --> D["File core.clj"]
This matters because Clojure loads namespaces from the JVM classpath. If the namespace and file path disagree, the error often looks like a classpath problem even when the real problem is naming.
A source namespace:
1(ns hello-world.core)
2
3(defn greeting [name]
4 (str "Hello, " name "!"))
A matching test namespace:
1(ns hello-world.core-test
2 (:require [clojure.test :refer [deftest is testing]]
3 [hello-world.core :as core]))
4
5(deftest greeting-test
6 (testing "builds a greeting"
7 (is (= "Hello, Ada!" (core/greeting "Ada")))))
The -test suffix is a convention, not a JVM requirement. It makes test discovery and human navigation easier.
| Source item | Test item |
|---|---|
src/hello_world/core.clj |
test/hello_world/core_test.clj |
hello-world.core |
hello-world.core-test |
greeting |
greeting-test |
deps.edn StructureA practical first deps.edn might look like this:
1{:paths ["src" "resources"]
2 :deps {org.clojure/clojure {:mvn/version "1.12.4"}}
3 :aliases
4 {:test {:extra-paths ["test"]}
5 :run {:main-opts ["-m" "hello-world.core"]}}}
For a Java engineer, the main shift is that paths are explicit classpath roots. Test code is not automatically visible unless you include it through an alias or tool-specific convention.
deps.edn key |
What to watch |
|---|---|
:paths |
Production classpath roots |
:deps |
Libraries resolved from Maven, Git, or local coordinates |
:aliases |
Optional classpath changes or command options |
:extra-paths |
Additional roots such as test or dev |
:main-opts |
Main-class-like command behavior for clojure -M |
A Leiningen project has a similar source layout but uses project.clj:
1hello-world/
2├── project.clj
3├── resources/
4├── src/
5│ └── hello_world/
6│ └── core.clj
7└── test/
8 └── hello_world/
9 └── core_test.clj
The practical difference is ownership:
| Concern | Clojure CLI | Leiningen |
|---|---|---|
| Dependency file | deps.edn |
project.clj |
| Run command | clojure -M -m ... or alias |
lein run |
| Test command | Alias or test runner | lein test |
| Packaging | Usually tools.build or another build tool |
lein uberjar |
Learn both layouts enough to read an existing repository. Pick one default only when creating a new project.
| Mistake | Symptom | Fix |
|---|---|---|
| Namespace has a hyphen but path also uses a hyphen | Namespace not found | Use underscores in the path |
| Test path is not on classpath | Test namespace cannot load | Add test through an alias or tool convention |
Function lives in user during REPL work only |
Code disappears on restart | Move it into a real namespace |
| Java-style deep package tree is copied mechanically | Too many tiny namespaces | Group by behavior and data flow, not class count |
resources/ is omitted from paths |
Config files are not found | Add resources to :paths |
deps.edn and project.clj are both project configuration files; do not mix them casually in one small project.