{"id":24353751,"url":"https://github.com/adityaathalye/clojure-multiproject-example","last_synced_at":"2025-07-18T19:06:59.633Z","repository":{"id":272294427,"uuid":"912500388","full_name":"adityaathalye/clojure-multiproject-example","owner":"adityaathalye","description":"A grug-brained stab at layout and tooling to conveniently develop many Clojure projects in a single source repo.","archived":false,"fork":false,"pushed_at":"2025-03-10T12:18:17.000Z","size":537,"stargazers_count":17,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-10T01:12:10.995Z","etag":null,"topics":["clojure","demo-project","example-code","expression-problem","frameworks","full-stack","grug","monolithic-architecture","object-functional","webapp"],"latest_commit_sha":null,"homepage":"https://www.evalapply.org/posts/clojure-web-app-from-scratch/","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adityaathalye.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-05T18:37:24.000Z","updated_at":"2025-03-10T12:18:20.000Z","dependencies_parsed_at":"2025-01-13T14:46:08.049Z","dependency_job_id":"7f81be78-8426-475e-97c0-ef9296845100","html_url":"https://github.com/adityaathalye/clojure-multiproject-example","commit_stats":null,"previous_names":["adityaathalye/clojure-multiproject-example"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/adityaathalye/clojure-multiproject-example","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adityaathalye%2Fclojure-multiproject-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adityaathalye%2Fclojure-multiproject-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adityaathalye%2Fclojure-multiproject-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adityaathalye%2Fclojure-multiproject-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adityaathalye","download_url":"https://codeload.github.com/adityaathalye/clojure-multiproject-example/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adityaathalye%2Fclojure-multiproject-example/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265816027,"owners_count":23833065,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","demo-project","example-code","expression-problem","frameworks","full-stack","grug","monolithic-architecture","object-functional","webapp"],"created_at":"2025-01-18T16:21:27.442Z","updated_at":"2025-07-18T19:06:59.584Z","avatar_url":"https://github.com/adityaathalye.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Clojure Multi-Project Example Layout and Tool Use\n\n![three REPL contexts, all started from multiproject's root](./doc/multiproject-multiple-repl-contexts.png)\n\n**NOTICE: Bespoke Software For My Use**.\n\nHowever, I hope others find some value in the idea(s) / trick(s)\ncontained herein. Plus, *\"Learn Generously\"* is [my\ncredo](https://www.evalapply.org/), and I hope publishing this\nwelcomes brain-exchange with kindred spirits.\n\nSo... MIT-licensed for great good.\n\nPlease mess about and [contribute](#contributing) feedback / critique!\nNo idea is sacrosanct.\n\n- **Try-out-ers**: Follow [Quick Start](#quick-start) to, well, quick\n  start.\n- **Rationale-seekers**: [Concept](#concept) and [sundry design notes](sundry-design-notes)\n  hopefully explain where I'm trying to go with this.\n- **Code readers**, these are the important reference points:\n  - The top-level `deps.edn` declares all source code relationships\n  - `build/build.clj` and `bin/run_cmd.sh` contain all commands to\n    execute against the whole or part of the multiproject\n  - **`parts` are *functions***: Mutually independent building blocks\n    that we configure and compose into applications. I've made my own\n    `grugstack`, but you can vendor in anything of your own.\n  - **`projects` are *objects***: Purpose-built applications, having\n    whatever architecture they need to have, built using whatever set\n    of libraries and/or `parts` they need.\n  - *Following multiproject's `:app-alias` naming convention is key\n    (haha) to success* (see [MultiProject Conventions](#multiproject-conventions)).\n\n# Quick Start\n\nTry from project root:\n\n- [Requirements](#requirements) must first be satisfied, of course.\n- The `bin/run_cmd.sh` Bash helper script builds and runs commands\n  documented in [Usage](#usage). (TBD: rewrite as babashka script for\n  maximum portability).\n\n```shell\nbin/run_cmd.sh # and follow the menu to pick COMMAND and ALIAS\n\n# OR\nbin/run_cmd.sh run_TESTS # and follow the menu to pick ALIAS\n\n# OR, directly make it do what you mean...\n\nbin/run_cmd.sh run_TESTS \"com.acmecorp.snafuapp.core\"\n\nbin/run_cmd.sh run_UBERJAR \"com.acmecorp.snafuapp.core\"\n```\n\n# Concept\n\nAs of today, **\"multiproject\" addresses my own specific\nrequirements;** viz.\n\n- Indie and Hobby SaaS apps of my own;\n\n- [RAD](https://en.wikipedia.org/wiki/Rapid_application_development \"Rapid Application Development\")\n  for outcomes-driven customers who want long-lived, stable\n  software. I'm my own customer! And **I'm available for hire!**\n\n- Benefit from the Clojure ecosystem's creativity, reach, stewardship,\n  *and* software stability.\n\n**Professionally**, I want to be able to:\n\n- create, manage, and maintain multiple apps/projects,\n- under /a single source tree/ (no diamond dependencies, please),\n- crafted with a well curated application stack\n- that each application/project can use a la carte\n- *and* which I can pry apart cleanly, if needed (e.g. hand over source to a\n  customer or buyer (that will be the day!)).\n\n**Architecturally**, I want to retain optionality.\n\nChoosing to focus on *outcomes* means I should retain the option to\nuse *any* framework or architecture or Clojure dialect, for any type\nof application (web, backend, developer tool etc.).\n\nAs of this writing, I feel like this is already plausible. I don't see\nwhy any one app managed by \"multiproject\" cannot itself be a\nbiff/kit/duct app, or itself follow polylith architecture, or be just\na single babashka script.\n\n**Systematically**, I want /system and workflow/ re-use.\n\nIn addition to stack reuse.\n\nJust enough structure to help me manage things, while staying sane,\nwithout building hard dependence on any specific project layout, build\ntool, application stack etc.\n\nMore stream-of-conscious-y stuff squirreled away under\n[sundry design notes](#sundry-design-notes).\n\n# Requirements\n\n## Mandatory\n\nA [working Clojure\ninstallation](https://clojure.org/guides/install_clojure \"Clojure\ninstall instructions\") (prefer the latest available Clojure).\n\n## Optional\n\nBash to use the `bin/run_cmd.sh`. I've assumed Bash 5.0+. Bash 4.0+\nshould be okay too.\n\n## DIY\n\nNon-Linux users may have to fiddle about to make things work right for\nthem. I use Ubuntu 24.04 LTS on my dev and server boxen. So I haven't\npaid attention to any other OS.\n\n# MultiProject Conventions\n\n## Mandatory\n\n**The root `deps.edn` is the lynchpin of control.**\n\n- It defines conventions and data relied upon by everything under the\n  multiproject; repl, build, test, ci, local-m2 etc.\n- Each project/app must have a minimal `deps.edn` under its project\n  root. The app-specific deps file must specify its own library\n  dependencies.\n- Multiproject commands will ignore any project-specific alias or\n  extra path. These may be defined at-will, only for developer\n  convenience.\n\n**Top-level alias naming convention does *a lot* of heavy lifting for us.**\n\n  - All project-global information must be described under `:root/`\n    aliases, only in the root `deps.edn` file. This helps avoids\n    overlap / conflict with system-specific and user-specific\n    deps.edn.\n  - Each project / application must get its own alias (a keyword),\n    only in the root `deps.edn`. The alias data is used by commands\n    defined for use within the multiproject.\n  - Project alias names *must* map to one and only one project.\n  - Project alias name must match the project' main entry point namespace.\n    - Build commands rely on the keyword alias when provided\n      via. `:app-alias` command line option.\n    - The uberjar build procedure relies on our namespacing convention\n      to ensure clean separation of build-time classpaths, intermediate\n      artifacts, and so forth.\n    - Socket REPL management is easier if we name the socket file\n      according to the alias.\n\n## Optional\n\n- A multiproject-local `.m2` repository may be useful for dependency\n  management / audit reasons, but isn't a hard requirement.\n- We many need to help LSP recognise individual apps/projects by\n  placing a `.gitignore` under the project root (for completions,\n  refactoring, etc.).\n\n# Usage\n\nUse aliases from the root-level deps.edn to run everything from the\nroot of the source repo. Here \"everything\" including tests, builds, CI\ntasks.\n\n## REPL Development\n\nI prefer to start REPLs at the shell, and connect to them via my code\neditor. This lets me independently kill/restart/manage the REPL\nprocess and the Editor process. Sometimes one of them can go into a\nbad state. Live REPL state tends to collect orphan objects the longer\nthey are live. Sometimes I want to force-invalidate some editor\nstate, which may require killing and cold-starting it. etc...\n\n### Start a REPL at a random port.\n\nThis is the most common way to start a REPL. This works just fine for\nconventional single-repo single-app style projects.\n  ```shell\n  clj -M:root/all:root/dev:root/test:cider\n  ```\n### Start REPL at specific UNIX domain socket.\n\nThis is my preferred tactic to trivially share or isolate REPLs from\neach other, in a multi-project context. The trick is to name socket\npaths along the project directory paths structure. For example:\n- To imply a REPL is *shared across the whole multi-project*, create\n  the UNIX domain socket at the root of the multi-project repo.\n\n  ```shell\n  clj -M:root/all:root/dev:root/test:cider --socket \"repl.socket\"\n  ```\n- To imply a REPL is *specific to a project*, create the socket at the\n  root of the project directory.\n\n  ```shell\n  clj -M:root/all:root/dev:root/test:com.example.core:cider --socket \"projects/example_app/repl.socket\"\n\n  clj -M:root/all:root/dev:root/test:com.acmecorp.snafuapp:cider --socket \"projects/acmecorp/snafuapp/repl.socket\"\n  ```\n\n## REPL Development with Project Isolation\n\nThis trick helps us conveniently access / share code from anywhere in\nthe multi-project, while also being able to isolate project-specific\nREPL state when needed.\n\nExample: Isolating Integrant REPL state per-project, while being able\nto access source code from any path in the multi-project.\n\n- We know we can automatically bootstrap custom code and settings into\n  the default `user` namespace. This is Clojure's standard bootstrap\n  behaviour.\n- We know every REPL is an isolated process. So, all bootstrapped\n  context is isolated by construction.\n\nGiven these guarantees...\n\n- I've placed my \"bootstrap\" code in `dev/user.clj`, right under the\n  root of the multi-project. It contains handy REPL utilities to\n  manipulate `system` state (start / stop / restart), as well as spin\n  up developer tools like *portal*.\n- Thus different REPLs running at independent sockets, are\n  bootstrapped using the same `user.clj` code *yet* REPL state is\n  maintained independently, in isolated process, by construction.\n- *At the same time*, we can use our top-level `deps.edn` settings to\n  configure source access for each project-specific alias. With this,\n  we can grant access to any permutation of the multi-project\n  codebase.\n\nThus, inert code can be shared, while live state is isolated.\n\n## TEST running\n\n- Test all apps:\n  ```shell\n  clj -X:root/all:root/test\n  ```\n  - The trick is to declare all test target directories; viz. root of\n    all apps and of grugstack. PLUS, inject grugstack as an extra-deps\n    (via :root/all).\n\n- Test only one app:\n  - To narrow test target to a single project at a time, we rely on\n    the published alias override/merge behaviour of deps.edn.\n  - Here do it for SNAFUapp by ACME Corp. By appending its deps.edn\n    alias, we narrow test target to only SNAFuapp test dirs. This is\n    due to the \"win last\" override/merge order of aliases\n    [defined by Clojure CLI](https://clojure.org/reference/clojure_cli#aliases).\n  ```shell\n  clj -X:root/all:root/test:com.acmecorp.snafuapp.core\n  ```\n  - Here we do the same narrowing for `grugstack` itself.\n  ```shell\n  clj -X:root/all:root/test:grugstack\n  ```\n\n## BUILD options\n\n### Full CI\n\nRun full CI sequence for individual apps.\n\n- BUILD default example project specified by alias under this path in\n  our deps config -\u003e [:root/build :extra-args :app-alias].\n  ```shell\n  clj -T:root/build\n  ```\n  Then check it with...\n  ```\n  java -jar target/com.example.core/com.example.core-*.jar\n  ```\n- BUILD project identified by :app-alias\n  ```\n  clj -T:root/build :app-alias ':com.acmecorp.snafuapp.core'\n  ```\n  Then check it with...\n  ```\n  java -jar target/com.acmecorp.snafuapp.core/com.acmecorp.snafuapp.core-*.jar\n\n  # Followed by\n\n  curl localhost:13337\n  ```\n\n### UBERJAR only\n\nPackage a single app into an uberjar.\n\nRun only \"uberjar\" part of the CI sequence, for app identified by\n`:app-alias`.\n\n```shell\nclj -T:root/build uberjar :app-alias ':com.acmecorp.snafuapp.core'\n```\n\nThen check it as described above\n\n### TEST only\n\nRun only \"test\" part of the CI sequence, for apps identified by\n`:app-alias`.\n\n```shell\nclj -T:root/build test :app-alias ':com.acmecorp.snafuapp.core'\n```\n\n## NEW PROJECT addition\n\nWe can just use the\n[deps-new](https://github.com/seancorfield/deps-new) tool, from the\nroot directory of the multi-project repo.\n\nExample:\n\n```shell\nclojure -Tnew app :name com/acmecorp/fubarapp/core :target-dir projects/acmecorp/fubarapp\n```\n\nThis produces the usual \"application\" project layout. We don't really\n*need* some of the files under the newly-created project, but it\ndoesn't hurt us to let them be (README, LICENSE, build.clj, .gitignore\netc.). Tomorrow, if I need to pull the app out of the multi-project,\ninto a standalone project (say, to hand over to someone), then staying\ncompatible with the standard app layout should make extraction easier.\n\n```text\n$ tree -a projects/acmecorp/fubarapp/\nprojects/acmecorp/fubarapp/\n├── build.clj\n├── CHANGELOG.md\n├── deps.edn\n├── doc\n│   └── intro.md\n├── .gitignore\n├── LICENSE\n├── README.md\n├── resources\n│   └── .keep\n├── src\n│   └── com\n│       └── acmecorp\n│           └── fubarapp\n│               └── core.clj\n└── test\n    └── com\n        └── acmecorp\n            └── fubarapp\n                └── core_test.clj\n```\n\n# Sundry Project Maintenance\n## Code formatting\n\nAs of this writing, `cljfmt` drives code formatting convention. Thank\nyou, yet again, [weavejester](https://github.com/weavejester).\n\n- Install as clj tool aliased as cljfmt:\n```shell\nclj -Ttools install io.github.weavejester/cljfmt '{:git/tag \"0.13.0\"}' :as cljfmt\n```\n- Configure options in `cljfmt.edn` (`:path` being the most important\n  for our multi-project).\n- Check whole-repo formatting: `clj -Tcljfmt check`\n- Fix whole-repo code format: `clj -Tcljfmt fix`\n# Sundry Design Notes\n\nHere's my stream of consciousness...\n\nThe attempt is to...\n- Share a common abstract \"system\" (which I'm calling `grugstack`),\n- across multiple apps, within a single repo (this multi-project example),\n- managed with only out-of-the-box tooling (not out-of-the-box thinking).\n- Whether the apps are standalone (like `projects/example_app`), or must\n  be neatly isolated for my customers (e.g. `projects/acmecorp/snafuapp`).\n  - BTW, *do you want to build a niche Micro-SaaS?* For outside\n    customers? Inside customers? As your own private force-multiplier?\n    *[Hire me!](mailto:adiDELET3ALLCAP1TAL5ANDNUMBER5%40evalapply.org?subject=Build%20us%20a%20Micro%20SaaS%20using%20boringly%20stable%20technology.)*\n\nConceptually, it is aspirational...\n- I was obsessing about web stacks, but now I suspect this bag of\n  tricks will generalise to apps made for any arbitrary Clojure /\n  ClojureScript runtime; whether web app, desktop, mobile, cli, data\n  science etc... And the code for all these diverse apps can be\n  managed within a single source repo.\n- The *Way* is not Purely Functional /or/ Purely Object Oriented, but\n  a \"Best of Both\" approach, all the way down to code layout.\n  - A system of parts (functions) that glue together *à la carte*,\n  - via carefully constructed (namespaced, structured) plain Clojure\n    data,\n  - into any number of runnable apps (open-ended polymorphism) (see\n    [credits](#credits)).\n\nIdeally, I want to get away with...\n- **the simplest possible project tree layout**, even if it makes code\n  and command-line invocations repetitive or verbose. My grug brain\n  can easily plod along long paths, as long as they are made painfully\n  obvious.\n- **the fewest possible bespoke abstractions or terms of art**,\n  because making a Domain Specific Language is easy, but keeping it\n  sensible is hard.\n- **no custom build tooling.** I just want to appropriate the raw\n  machinery and public contracts provided by deps.edn, tools.build,\n  and Clojure CLI. I've chosen to used in the usual way, via a single\n  multi-project-level `build.clj`. Stare at the `build.clj` file and\n  the multi-project-level `deps.edn` file side-by-side to see the\n  main trick I've used... I figured out a permutation of (alias names\n  x grouping of paths x grouping of dependencies x grouping of args\n  that must get overridden to narrow context to the last alias).\n\nBy design, I want to run tools / start REPLs etc. ***only at the root\nof the multi-project***, and use explicit command-line options to\nbroaden or narrow scope of the command / REPL to the part(s) of the\nmulti-project that I want to target. This helps me keep a simple\nmental model of managing the whole or the part of the multi-project,\nthat can show up legibly in my shell's history / project logs /\ndocs etc. (hopefully the [Usage](#usage) examples will make this clear).\n\n# TODO\n\nLots.\n\n# Contributing\n\nAs of now, I am in discussions-only mode... Ping me in the Clojurians\nSlack or Zulip or [this thread](https://groups.google.com/g/clojure/c/uroL-ftfrqY)\nin the official mailing list.\n\n# Credits\n\nI've consumed way too much web-stack buildin' prior art from across\nthe Clojure ecosystem. Clojure core, community librarians, builders,\narchitects, teachers, book authors: the whole lot of you; thank you!\n\nSpecial mentions to authors making projects and explanations for use\nby solo/indie web app builders: biff, duct, zodiac, caveman.\n\nLast but not least, special mention to `polylith`, which lit a key\nlight bulb in my head... The \"Expression Problem\" applies to code\nlayout and application architecture too! Not Functions v/s Objects;\nbut Functions *and* Objects.\n\nThat said, I'm not sure I've \"got it\" yet. So to take it back to the\ntop, please [send design notes my way](#)!\n\n# License \u0026 Copyright\n\nCopyright (c) 2025 Aditya Athalye.\n\nDistributed under the MIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadityaathalye%2Fclojure-multiproject-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadityaathalye%2Fclojure-multiproject-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadityaathalye%2Fclojure-multiproject-example/lists"}