Practical ways to structure Clojure projects so namespaces, files, and boundaries stay readable as the system grows.
Code organization in Clojure is less about pleasing a style guide and more about making change cheap. A well-organized project tells readers where business rules live, where effects happen, how namespaces depend on one another, and which files are safe to touch for a given feature.
Clojure gives teams a lot of freedom, which is useful but also risky. If everything lands in core, util, or a catch-all helpers namespace, the codebase may still run, but navigation, testing, and refactoring get steadily worse. The goal is not maximal cleverness. The goal is predictable boundaries.
The strongest first question is not “What kind of code is this?” but “What tends to change together?”
If a feature changes as a unit, its code should probably live close together. That often leads to feature-oriented namespaces such as:
1src/my_app/orders/
2src/my_app/billing/
3src/my_app/auth/
Within each area, you can still split by responsibility:
1src/my_app/orders/
2├── api.clj
3├── domain.clj
4├── persistence.clj
5└── workflows.clj
This approach is usually easier to evolve than one giant cross-cutting layout like controllers, services, and utils for the entire system, because a reader can stay inside one business area longer before jumping between unrelated files.
Layered organization is still useful, especially in teams that want explicit boundaries between HTTP, domain logic, and storage. The mistake is not using layers. The mistake is forcing a layer model that scatters one feature across too many files before the project is large enough to justify it.
A namespace should have a job you can explain in one sentence.
This is a healthy namespace:
1(ns my-app.orders.persistence
2 (:require [next.jdbc :as jdbc]
3 [my-app.db :as db]))
Its responsibility is obvious: persistence for orders.
This is a weaker namespace:
1(ns my-app.utils)
utils usually means “we did not decide where these things belong.” That may be tolerable for a tiny spike or a throwaway script, but in an application it tends to become a junk drawer.
Good namespace names:
misc, helpers, or commonModern Clojure projects usually center on deps.edn, explicit aliases, and a clear separation between source, test, and resources.
1my-app/
2├── deps.edn
3├── src/
4│ └── my_app/
5│ ├── core.clj
6│ ├── orders/
7│ │ ├── api.clj
8│ │ ├── domain.clj
9│ │ └── persistence.clj
10│ ├── billing/
11│ │ ├── api.clj
12│ │ └── service.clj
13│ └── http/
14│ ├── routes.clj
15│ └── server.clj
16├── test/
17│ └── my_app/
18│ ├── orders/
19│ │ ├── api_test.clj
20│ │ └── domain_test.clj
21│ └── billing/
22│ └── service_test.clj
23└── resources/
24 └── config.edn
That layout does three useful things:
The exact shape can vary, but the reader should not have to guess which files are application code, which are tests, and which are deployment or config inputs.
One of the highest-value organizational moves in Clojure is separating pure decision-making from side-effecting work.
For example:
1(ns my-app.orders.domain)
2
3(defn payable? [order]
4 (and (= :ready (:status order))
5 (pos? (:total-cents order))))
1(ns my-app.orders.api
2 (:require [my-app.orders.domain :as domain]
3 [my-app.orders.persistence :as persistence]))
4
5(defn mark-payable! [db order-id]
6 (when-let [order (persistence/find-order db order-id)]
7 (when (domain/payable? order)
8 (persistence/update-status! db order-id :payable))))
This split makes code organization better for reasons beyond aesthetics:
In practice, this often matters more than whether a namespace is named service, logic, or workflow.
As a project grows, the real problem is often not file count. It is dependency shape.
Warning signs:
core knows too much about everythingThis is the shape you want more often:
flowchart LR
A["HTTP / CLI Entry Points"] --> B["Feature API"]
B --> C["Domain Logic"]
B --> D["Persistence / External Services"]
D --> E["Database / Queue / HTTP Client"]
Readers can reason about this graph. Side effects stay near the edge. Domain rules stay readable. The project becomes easier to test and evolve.
Early in a project, a util namespace can be harmless. Later, it becomes a tax.
Refactor a catch-all namespace when:
Instead of one util namespace, split by reason:
my-app.timemy-app.moneymy-app.orders.formattingmy-app.http.responsesThat does not mean every helper deserves its own file. It means helpers should live near the domain or technical concern they actually support.
Tests are part of code organization, not a separate concern.
In Clojure, the simplest rule is usually the best one: mirror the production namespace layout where it stays intuitive.
1src/my_app/orders/domain.clj
2test/my_app/orders/domain_test.clj
That convention helps a lot during navigation, especially in editor tooling and REPL-driven work. A reader should be able to move from a production namespace to its tests without hunting.
For integration and end-to-end tests, it is also reasonable to keep dedicated top-level areas such as:
test/my_app/integration/test/my_app/contract/test/my_app/system/The important part is naming the test scope honestly.
Good Clojure structure works well with interactive development.
That means:
comment blocks and small entry points live near the code they help inspectIf every REPL experiment requires launching half the system, the organization is probably pushing too much work into the same namespace.
core.clj as the home for all important logicutil become the largest file in the project