Browse Learn Clojure Foundations as a Java Developer

Understand Clojure Project Structure

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.

The Minimal Shape

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

Namespace to File Mapping

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.

Source and Test Namespaces

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 Structure

A 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

Leiningen Project Structure

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.

Common Structure Mistakes

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

Key Takeaways

  • Clojure projects are organized around classpath roots and namespaces.
  • Namespace names map predictably to file paths.
  • Test namespaces should mirror source namespaces, but test paths must be included deliberately.
  • deps.edn and project.clj are both project configuration files; do not mix them casually in one small project.
  • Keep the first project small enough that namespace and classpath mistakes are easy to see.

Quiz: Project Structure

### What does `src/` usually represent in a Clojure CLI project? - [x] A production classpath root - [ ] A compiled class output directory only - [ ] A mandatory Maven package directory - [ ] A directory ignored by the Clojure CLI > **Explanation:** `src` is commonly listed in `:paths`, making it a classpath root for production namespaces. ### Where should the namespace `hello-world.core` usually live? - [x] `src/hello_world/core.clj` - [ ] `src/hello-world/core.clj` - [ ] `src/hello/world/core.java` - [ ] `test/hello_world/core.clj` > **Explanation:** Dots become directories and hyphens become underscores in the file path. ### Why might a test namespace fail to load in a Clojure CLI project? - [x] The `test` directory is not included on the classpath. - [ ] Clojure does not support test namespaces. - [ ] Test files must be Java files. - [ ] `deps.edn` cannot define aliases. > **Explanation:** Test paths are commonly added through an alias such as `:test`.
Revised on Saturday, May 23, 2026