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 build.clj under the workspace root:
(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.