How to start a Polylith project from scratch
A complete step-by-step guide to creating project called cat (or workspace, in Polylith terms) with a filesystem component, a main base, and a cli project using the Polylith architecture.
What You Will Build
cat/ ← workspace root
├── components/
│ └── filesystem/ ← reads a file and prints its content
├── bases/
│ └── main/ ← entry point (-main function)
└── projects/
└── cli/ ← deployable artifact (uberjar)
Data flow: java -jar cli.jar myfile.txt → main/-main → filesystem/read-file → stdout
Prerequisites
Install the following before starting:
- Java 21+ — verify with
java -version - Clojure CLI — install from clojure.org/guides/getting_started, verify with
clojure --version - git — verify with
git --version; also configureuser.nameanduser.email:git config --global user.name "Your Name" git config --global user.email "you@example.com" - poly tool — see below
Installing the poly tool
macOS:
brew install polyfy/polylith/poly
For other OS/platforms please refer to the official Installation doc.
Verify:
poly version
Step 1 — Create the Workspace
Run this outside any existing git repository:
poly create workspace name:cat top-ns:com.acme :commit
Move into the workspace:
cd cat
Your directory structure will look like:
cat/
├── .git/
├── .gitignore
├── bases/
├── components/
├── deps.edn
├── development/
│ └── src/
├── projects/
├── readme.md
└── workspace.edn
Enable auto-add in workspace.edn
Open workspace.edn and set :auto-add to true so that files generated by poly create commands are automatically staged in git:
{:top-namespace "com.acme"
:interface-ns "interface"
:default-profile-name "default"
:dialects ["clj"]
:compact-views #{}
:vcs {:name "git"
:auto-add true} ;; <-- change this to true
:tag-patterns {:stable "^stable-.*"
:release "^v[0-9].*"}
:template-data {:clojure-ver "1.12.0"}
:projects {"development" {:alias "dev"}}}
Step 2 — Create the filesystem Component
poly create component name:filesystem
This creates:
components/filesystem/
├── deps.edn
├── src/
│ └── com/acme/filesystem/
│ └── interface.clj
└── test/
└── com/acme/filesystem/
└── interface_test.clj
2a. Write the interface (public API)
The interface namespace is the only file other bricks are allowed to call. Edit components/filesystem/src/com/acme/filesystem/interface.clj:
(ns com.acme.filesystem.interface
(:require [com.acme.filesystem.core :as core]))
(defn read-file
"Reads the file at `filename` and prints its content to stdout."
[filename]
(core/read-file filename))
2b. Write the implementation
Create the file components/filesystem/src/com/acme/filesystem/core.clj:
(ns com.acme.filesystem.core
(:require [clojure.java.io :as io]))
(defn read-file
"Reads the file at `filename` and prints its content to stdout."
[filename]
(let [file (io/file filename)]
(if (.exists file)
(println (slurp file))
(println (str "Error: file not found — " filename)))))
2c. Register the component in the root deps.edn
Open the root ./deps.edn and add the filesystem component:
{:aliases {:dev {:extra-paths ["development/src"]
:extra-deps {com.acme/filesystem {:local/root "components/filesystem"}
org.clojure/clojure {:mvn/version "1.12.0"}}}
:test {:extra-paths ["components/filesystem/test"]}
:poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"]
:extra-deps {polyfy/clj-poly {:mvn/version "0.3.32"}}}}}
Step 3 — Create the main Base
poly create base name:main
This creates:
bases/main/
├── deps.edn
├── src/
│ └── com/acme/main/
│ └── core.clj
└── test/
└── com/acme/main/
└── core_test.clj
3a. Write the base code
A base differs from a component in that it has no interface — it is the entry point to the outside world. Edit bases/main/src/com/acme/main/core.clj:
(ns com.acme.main.core
(:require [com.acme.filesystem.interface :as filesystem])
(:gen-class))
(defn -main
"Entry point. Accepts a filename as the first argument and prints its content."
[& args]
(if-let [filename (first args)]
(filesystem/read-file filename)
(println "Usage: cat <filename>"))
(System/exit 0))
Key points:
(:gen-class)tells the Clojure compiler to generate a Java class with amainmethod.- The base calls
com.acme.filesystem.interface/read-file— never thecorenamespace directly. System/exit 0ensures the JVM terminates cleanly after running.
3b. Register the base in the root deps.edn
Add the main base alongside filesystem:
{:aliases {:dev {:extra-paths ["development/src"]
:extra-deps {com.acme/filesystem {:local/root "components/filesystem"}
com.acme/main {:local/root "bases/main"}
org.clojure/clojure {:mvn/version "1.12.0"}}}
:test {:extra-paths ["components/filesystem/test"
"bases/main/test"]}
:poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"]
:extra-deps {polyfy/clj-poly {:mvn/version "0.3.32"}}}}}
Step 4 — Create the cli Project
poly create project name:cli
This creates:
projects/cli/
└── deps.edn
4a. Register the project alias in workspace.edn
Open workspace.edn and add a cli alias to :projects:
:projects {"development" {:alias "dev"}
"cli" {:alias "cli"}}
4b. Wire the bricks into the project
Edit projects/cli/deps.edn to include the filesystem component, the main base, the uberjar entry point, and the build alias:
{:deps {com.acme/filesystem {:local/root "components/filesystem"}
com.acme/main {:local/root "bases/main"}
org.clojure/clojure {:mvn/version "1.12.0"}}
:aliases {:test {:extra-paths []
:extra-deps {}}
:uberjar {:main com.acme.main.core}}}
Step 5 — Add Build Support
The poly tool does not include a build command — it leaves artifact creation to your choice of tooling. We will use Clojure tools.build.
5a. Add the :build alias to the root deps.edn
Your final root ./deps.edn should look like this:
{:aliases {:dev {:extra-paths ["development/src"]
:extra-deps {com.acme/filesystem {:local/root "components/filesystem"}
com.acme/main {:local/root "bases/main"}
org.clojure/clojure {:mvn/version "1.12.0"}}}
:test {:extra-paths ["components/filesystem/test"
"bases/main/test"]}
:poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"]
:extra-deps {polyfy/clj-poly {:mvn/version "0.3.32"}}}
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"}}
:ns-default build}}}
5b. Create build.clj at the workspace root
Create the file cat/build.clj:
(ns build
(:require [clojure.tools.build.api :as b]
[clojure.java.io :as io]))
(defn uberjar
"Build an uberjar for a given project.
Usage: clojure -T:build uberjar :project cli"
[{:keys [project]}]
(assert project "You must supply a :project name, e.g. :project cli")
(let [project (name project)
project-dir (str "projects/" project)
class-dir (str project-dir "/target/classes")
;; Create the basis from the project's deps.edn.
;; tools.build resolves :local/root entries and collects all
;; transitive :paths (i.e. each brick's "src" and "resources").
basis (b/create-basis {:project (str project-dir "/deps.edn")})
;; Collect every source directory declared across all bricks.
;; basis :classpath-roots contains the resolved paths.
src-dirs (filterv #(.isDirectory (java.io.File. %))
(:classpath-roots basis))
main-ns (get-in basis [:aliases :uberjar :main])
_ (assert main-ns
(str "Add ':uberjar {:main <ns>}' alias to "
project-dir "/deps.edn"))
jar-file (str project-dir "/target/" project ".jar")]
(println (str "Cleaning " class-dir "..."))
(b/delete {:path class-dir})
(io/make-parents jar-file)
(println (str "Compiling " main-ns "..."))
(b/compile-clj {:basis basis
:src-dirs src-dirs
:class-dir class-dir})
(println (str "Building uberjar " jar-file "..."))
(b/uber {:class-dir class-dir
:uber-file jar-file
:basis basis
:main main-ns})
(println "Uberjar is built.")))
Step 6 — Validate the Workspace
Run the poly info command to see the current state of your workspace:
poly info
You should see both bricks (filesystem and main) listed, along with the cli project. Then validate the workspace integrity:
poly check
This should print OK. If there are errors, the command will describe what to fix.
Step 7 — Build the Uberjar
From the workspace root:
clojure -T:build uberjar :project cli
Expected output:
Compiling com.acme.main.core...
Building uberjar projects/cli/target/cli.jar...
Uberjar is built.
Step 8 — Run the CLI
Create a test file and run the app:
echo "Hello from Polylith!" > /tmp/hello.txt
java -jar projects/cli/target/cli.jar /tmp/hello.txt
Expected output:
Hello from Polylith!
Test the missing-file error path:
java -jar projects/cli/target/cli.jar /tmp/nonexistent.txt
Expected output:
Error: file not found — /tmp/nonexistent.txt
Test the no-argument path:
java -jar projects/cli/target/cli.jar
Expected output:
Usage: cat <filename>
Final Workspace Layout
cat/
├── bases/
│ └── main/
│ ├── deps.edn
│ └── src/com/acme/main/
│ └── core.clj ← -main, calls filesystem/read-file
├── components/
│ └── filesystem/
│ ├── deps.edn
│ └── src/com/acme/filesystem/
│ ├── interface.clj ← public API (read-file)
│ └── core.clj ← implementation
├── projects/
│ └── cli/
│ ├── deps.edn ← wires filesystem + main, :uberjar alias
│ └── target/
│ └── cli.jar ← generated artifact
├── build.clj ← tools.build script
├── deps.edn ← dev + test + poly + build aliases
└── workspace.edn ← top-ns, project aliases, vcs config
Key Concepts Summary
- Workspace is the monorepo root containing all bricks, in this project is
cat/ - Component is a reusable building block with a public
interfacens, such asfilesystem - Base is an entry-point brick that bridges the outside world to components, like
main - Project is a deployable artifact configuration; assembles bricks, the
cli - Interface is the only namespace other bricks may import from a component, like
com.acme.filesystem.interface
Useful poly Commands
poly info # overview of bricks and projects
poly check # validate workspace integrity
poly test # run all tests affected by recent changes
poly deps # show dependency graph
poly libs # show library usage
poly shell # interactive shell with autocomplete
Going Further
- Add more components — e.g.
poly create component name:parserfor argument parsing - Add tests — on a component level, add tests against the interface and not the implementation. You can have additional tests for the implementation to test internal functions etc. but use a different test file.
- Tag stable releases —
git tag stable-mainafter a cleanpoly test - CI integration — run
poly checkandpoly testin your pipeline; tag as stable on success - Multiple projects — add another project that reuses the same components
- Visit the official Polylith project documentation
- Read the Polylith book
- Ask questions and connect with other Polylith users on #polylith Slack channel.









Disabled code is gray, explanation is bright yellow

Notice how many colors there are. No one can remember that many.


Using italics and bold instead of colors
OkLab l=0.7473 c=0.1253 h=0, 45, 90, 135, 180, 225, 270, 315








What is going on?



