Map Clojure namespaces to source files, require aliases, and Java package instincts so namespace loading errors become easy to diagnose.
A Clojure namespace is the unit you normally load, require, reload, and alias. It plays part of the role a Java package plays, but it is not a class container. A namespace can hold functions, vars, macros, specs, constants, and interop helpers without wrapping them in a class.
The practical rule is simple: a namespace name must line up with the file path that can be found on the classpath.
| Clojure namespace | Expected file path |
|---|---|
orders.core |
src/orders/core.clj |
orders.http.api |
src/orders/http/api.clj |
order-service.core |
src/order_service/core.clj |
orders.core-test |
test/orders/core_test.clj |
The hyphen-to-underscore rule is the part Java engineers usually miss. Use hyphens in namespace names when they read well, but store those namespace segments with underscores in file names.
ns Form Sets the Loading BoundaryPut the namespace declaration at the top of the file. It tells Clojure which namespace receives the definitions in that file and which other namespaces should be available by alias.
1(ns order-service.pricing
2 (:require [clojure.string :as str]))
3
4(defn normalize-code [code]
5 (-> code
6 str/trim
7 str/upper-case))
For this namespace, the normal production file is:
1src/order_service/pricing.clj
Think of :require as closer to Java’s import plus classpath loading. The alias does not copy definitions into the current namespace. It gives you a short, explicit qualifier such as str/trim.
When Clojure sees (require 'order-service.pricing), it looks for a loadable resource that matches the namespace convention. The diagram below shows the path translation that matters during debugging.
flowchart TD
A["Namespace symbol"] --> B["order-service.pricing"]
B --> C["Dots become directories"]
C --> D["Hyphens become underscores"]
D --> E["Classpath resource"]
E --> F["order_service/pricing.clj"]
What to notice: Clojure does not scan every file looking for a matching (ns ...) form. It uses the namespace name to derive a resource path on the classpath. If the path is wrong, require fails before your function definitions matter.
Your Java package-navigation habit is useful, but it needs two adjustments.
| Java habit | Clojure adjustment |
|---|---|
| Package name mirrors directories | Namespace segments mirror directories, but hyphenated segments use underscores on disk |
| One public class anchors the file | One namespace anchors the file and can contain many public vars |
| Imports make names shorter | :require aliases keep names short without hiding ownership |
| Package cycles are a design smell | Namespace cycles are also a smell and can make REPL reloads painful |
The biggest design shift is that a namespace should usually describe a coherent set of functions and data transformations, not a single class. For example, order-service.pricing might contain pricing rules, helper functions, and pure calculations. It does not need PricingService, PricingUtils, and PricingCalculator classes just to organize behavior.
Prefer explicit aliases for namespaces you call frequently.
1(ns order-service.handler
2 (:require
3 [order-service.pricing :as pricing]
4 [order-service.validation :as validation]))
5
6(defn handle-price-request [request]
7 (let [order (:body request)]
8 (if-let [errors (seq (validation/order-errors order))]
9 {:status 400
10 :body {:errors errors}}
11 {:status 200
12 :body {:total (pricing/total order)}})))
This is more maintainable than pulling many names into the current namespace with :refer :all. Java developers often like broad imports because IDE navigation can hide the cost. In Clojure, qualified names communicate ownership directly in the code.
| Symptom | How to debug |
|---|---|
Could not locate order_service/pricing... |
Check that the file is under a classpath root such as src/, then verify dots, slashes, hyphens, and underscores. |
| Function appears undefined after edit | Reload the namespace, evaluate the file again, or use your editor’s reload command. |
| Circular require error | Move shared functions to a lower-level namespace or invert the dependency. |
Create src/order_service/pricing.clj and require it from a REPL.
1(ns order-service.pricing)
2
3(defn total [order]
4 (reduce + (map :price (:items order))))
Then evaluate:
1(require '[order-service.pricing :as pricing])
2
3(pricing/total {:items [{:price 12} {:price 8}]})
4;; => 20
If the require fails, debug the path first. If the call fails after require succeeds, debug the var name, argument shape, or namespace reload state.