Treat the REPL and main-style entry points as complementary tools: one for exploratory access to a live system, the other for external startup and deployment.
This comparison becomes much clearer once you stop treating the REPL and main as competitors.
They answer different questions.
main asks: how does an external caller start this program?For a Java engineer, that is the real shift.
In Java, public static void main(String[] args) is the normal door into a program.
That encourages a familiar loop:
mainThere is nothing wrong with that model. It is still useful for:
But it is a coarse-grained way to answer small development questions.
The REPL is a much more surgical entry point.
Instead of committing to one whole execution path, you can:
This is why Clojure development feels different even when the final application still has a -main function.
The runtime entry point for users may still be -main.
The day-to-day entry point for developers often becomes the REPL.
-main Still Matters In ClojureClojure has a -main convention for exactly the same practical reasons Java has main:
So the lesson is not “stop using -main.”
The lesson is “stop forcing every development question through -main.”
| Question | REPL | -main |
|---|---|---|
| “What does this one function do on this input?” | Excellent fit | Overkill |
| “Can I redefine this namespace and try again?” | Excellent fit | Usually awkward |
| “How does a user or script start the program?” | Poor fit | Excellent fit |
| “What should production invoke?” | Poor fit | Excellent fit |
| “How do I inspect one intermediate value?” | Excellent fit | Usually indirect |
That table is more useful than arguing that one model is simply better.
-mainHere is a Clojure shape that works well:
1(ns orders.cli
2 (:gen-class)
3 (:require [clojure.edn :as edn]
4 [orders.pricing :as pricing]))
5
6(defn -main [& [path]]
7 (let [order (-> path slurp edn/read-string)]
8 (println (pricing/order-total order))))
Notice the design:
-main handles the outside worldThat lets you use the same core function directly at the REPL:
1(require '[orders.pricing :as pricing])
2
3(pricing/order-total
4 {:order/id 1001
5 :items [{:sku "A-1" :qty 2 :unit-price 15M}
6 {:sku "B-2" :qty 1 :unit-price 9M}]})
7;; => 39M
This is the sweet spot for most Clojure applications:
-main for process startupThere are two common mistakes during the transition.
The first is bringing too much main thinking into Clojure:
The second is swinging too far the other way:
Both mistakes come from treating the two entry paths as rivals instead of companions.
Use the REPL when you want to:
Use -main when you want to:
In a healthy Clojure project, you use both constantly, just for different reasons.
The REPL improves the inner development loop because it cuts away startup overhead for small questions.
-main improves operational clarity because it gives the application a stable external boundary.
When those concerns are separated cleanly:
That is a strong architectural improvement, not just a tooling preference.