Clojure Code Organization

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.

Organize by Change, Not Just by Type

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.

Namespace Boundaries Should Explain Responsibility

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:

  • reflect domain or operational responsibility
  • match the directory layout cleanly
  • give readers a hint about side effects
  • avoid vague buckets like misc, helpers, or common

Use the File Tree to Support the Mental Model

Modern 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:

  • it mirrors namespace names cleanly
  • it keeps tests close to the conceptual units they verify
  • it separates code, configuration, and operational assets

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.

Keep Pure Logic Away from Effect Boundaries

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:

  • pure logic is easier to test
  • effect boundaries are easier to audit
  • changing storage or transport concerns does not force unnecessary domain rewrites

In practice, this often matters more than whether a namespace is named service, logic, or workflow.

Avoid Dependency Graphs That Grow Sideways Forever

As a project grows, the real problem is often not file count. It is dependency shape.

Warning signs:

  • feature namespaces depend on each other in circles
  • many namespaces reach directly into many persistence namespaces
  • core knows too much about everything
  • macros or shared helpers quietly pull in a wide swath of the system

This 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.

Refactor Buckets Before They Become Permanent

Early in a project, a util namespace can be harmless. Later, it becomes a tax.

Refactor a catch-all namespace when:

  • unrelated functions are accumulating there
  • changes to one feature keep touching the same generic file
  • readers cannot tell whether a helper is pure, effectful, or domain-specific
  • multiple teams are editing the same file for unrelated reasons

Instead of one util namespace, split by reason:

  • my-app.time
  • my-app.money
  • my-app.orders.formatting
  • my-app.http.responses

That does not mean every helper deserves its own file. It means helpers should live near the domain or technical concern they actually support.

Match Test Layout to Production Readability

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.

Code Organization Should Support the REPL

Good Clojure structure works well with interactive development.

That means:

  • namespaces load independently when possible
  • pure functions can be evaluated without booting the whole system
  • comment blocks and small entry points live near the code they help inspect
  • startup wiring is separate from reusable domain behavior

If every REPL experiment requires launching half the system, the organization is probably pushing too much work into the same namespace.

Common Mistakes

  • treating core.clj as the home for all important logic
  • organizing only by technical layer when the domain is the stronger unit of change
  • hiding business logic in persistence or transport namespaces
  • letting util become the largest file in the project
  • reusing helpers across contexts before proving the abstraction is stable
  • making namespace boundaries reflect team history instead of current system shape

Key Takeaways

  • Organize around change boundaries, not only file type.
  • Make namespace names explain responsibility.
  • Keep pure logic separate from effectful integration points.
  • Use the directory tree to make navigation and testing predictable.
  • Refactor generic buckets early, before they harden into architecture.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026