My Profile Photo

Bits and pieces


Between bits and bytes and all other pieces.
A tech blog about Clojure, Software Architecture, and Distributed Systems


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.txtmain/-mainfilesystem/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 configure user.name and user.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 a main method.
  • The base calls com.acme.filesystem.interface/read-filenever the core namespace directly.
  • System/exit 0 ensures 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 interface ns, such as filesystem
  • 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:parser for 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 releasesgit tag stable-main after a clean poly test
  • CI integration — run poly check and poly test in 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.
comments powered by Disqus