Articles on Clojure/Script architecture and patterns
Polylith is no doubt an attractive framework with huge benefits. However their documentation assumes you want to go all in. This article serves to guide the setup of your code so as to use two of Polylith's 'tricks' to achieve multiple implementations of an interface without runtime dispatch. Best used for an architectural level 'interface API' where choosing the implementation at build or REPL-start time is more benefit than hassle.
We will use the example of having two different database backends, PostgreSQL and Datomic, and one simple function q-by-ident. We will start with the directory structure then look at that from the point of view of deps.edn, and lastly look at the code.
project/
├── interfaces/ # Switchable facade layer
│ ├── storage/
│ │ └── src/your_domain/storage.clj # Generic storage API
│ └── users-storage/
│ └── src/your_domain/users_storage.clj # Users API
│
├── packages/ # Implementation layer
│ ├── storage-datomic/
│ │ └── src/your_domain/storage_datomic.clj
│ ├── storage-postgresql/
│ │ └── src/your_domain/storage_postgresql.clj
│ │
│ ├── users-storage-datomic/
│ │ └── src/your_domain/users_storage_datomic.clj
│ └── users-storage-postgresql/
│ ├── src/your_domain/users_storage_postgresql.clj
│ ├── src/your_domain/users_query.clj
│ └── src/your_domain/users_txn.clj
│
└── src/your-domain/ # Consumer code
├── starter_app/
│ └── auth.clj # Uses your-domain.users-storage
└── restaurant/
└── main.clj # Uses your-domain.storage
Here we are showing two APIs: storage and users-storage, implemented in PostgreSQL and Datomic. I'm using the term package, that in Polylith would be the implementation of a component. An equivalent Polylith directory structure would be more involved and require multiple deps.edn files.
This is where your project's classpath is setup. If I wanted to use the PostgreSQL API then I might have this as the :paths map-entry:
:paths
["src" "src-contrib" "resources"
"interfaces/storage/src" "packages/storage-postgresql/src"
"interfaces/users-storage/src" "packages/users-storage-postgresql/src"
]
For Datomic it would be this:
:paths
["src" "src-contrib" "resources"
"interfaces/storage/src" "packages/storage-datomic/src"
"interfaces/users-storage/src" "packages/users-storage-datomic/src"
]
I would then comment out the Datomic artifact coordinates from :deps and make sure the PostgreSQL ones are not commented out. As a more sophisticated deps.edn user you might employ separate aliases for datomic and postgresql each having :extra-paths and :extra-deps, and selectively use them for development and build.
The interface namespace acts as a switchboard, requiring and exposing functions from the chosen implementation:
File: interfaces/storage/src/your_domain/storage.clj
(ns your-domain.storage
(:require [your-domain.storage-postgresql :as impl]))
(def q-by-ident impl/q-by-ident)
Or for Datomic:
(ns your-domain.storage
(:require [your-domain.storage-datomic :as impl]))
(def q-by-ident impl/q-by-ident)
The implementations are of course completely different:
File: packages/storage-postgresql/src/your_domain/storage_postgresql.clj
(ns your-domain.storage-postgresql
(:require [next.jdbc :as jdbc]))
(defn q-by-ident [db-conn ident]
(let [[attr val] ident]
(jdbc/execute-one! db-conn
[(str "SELECT * FROM entities WHERE " (name attr) " = ?") val])))
File: packages/storage-datomic/src/your_domain/storage_datomic.clj
(ns your-domain.storage-datomic
(:require [datomic.api :as d]))
(defn q-by-ident [db ident]
(d/pull db '[*] ident))
Consumer code requires the interface, never the implementation:
File: src/starter_app/auth.clj
(ns starter-app.auth
(:require [your-domain.storage :as storage]))
(defn lookup-user [sys username]
(storage/q-by-ident sys [:user/username username]))
To switch backends: change the require in the interface namespace, update your deps.edn paths, and restart your REPL. Consumer code remains unchanged.
This approach gives you Polylith's core architectural benefits - complete backend isolation and zero runtime overhead - without the need to put your entire codebase into Polylith's structure and without the complexity of the Polylith CLI toolchain or multiple deps.edn files. The two key tricks are:
Polylith offers sophisticated change detection, incremental testing and multiple deployable artifacts from the same codebase. When you need those features, you're already most of the way there - the working code exists, you just need to reorganize into Polylith's structure.
Published: 2025-12-09
With Fulcro the props always flow down from the base of your UI's tree structure. Thus all components/views will be refreshed, even those that are not currently visible. Introducing Fulcro's own router into your code-base is how you stop non-visible components being unnecessarily refreshed.
Re-frame doesn't have this story, so my thought was to not use any router until I hit a problem, despite the examples usually using a router. Initially I had 'navigation' app-state kept in many top level keys. Then I put them all under a key. Not much better! The third iteration, and the point of this post, has one key that describes the current path through the tree as a series of steps, from base to leaf. I used the key :at . Example two step app-state map-entry:
:at [[:tab :hamburger] [:hamburger :status]]
With this in place there's one reactive subscription for your app and one data-structure for user navigation events to alter.
You can have events that go to the base, across, or down to a new deeper level. Your views can pick apart the data-structure returned from the one subscription. Both the events and the queries upon this data-structure can be done with generic names, making the navigation flow of your app easy to read and easy to navigate from your IDE. The query functions I've needed so far are leaf-keyword, base-step, leaf-step, equals and predicate.
I'm proposing making a small Re-frame only client router library out of the code I'm using now. I say Re-frame only but this router will be easily adaptable to Reagent as well, directly using a ratom rather than a subscription. The rest of this article will be showing the code in use. It needs a name so Atrun, short for 'All the router U need'. Just a prospective name, I'll change it as soon as I find out its a lie!
Refactoring from 'something else' to this scheme can at first be done additively. The dispatch-able event in some view before:
[::r-events/inject-true [:nav :my-shop?]]
After:
[::at/down
[:hamburger :my-shop]
[[r-events/inject* [[:nav :my-shop?] true] true]]]
Explanation time. Fulcro has a convention that a function that takes and returns app-state end with a *. All reg-event-db events can call such functions and be done:
(defn inject* [st path value]
(s/setval path value st)
(reg-event-db
::inject-true
(fn [st [_ path]]
(inject* st path true)))
All the at events can take extra st->st functions. Mutating state in other ways whilst navigating is the bread and butter of any UI program. Here we are just making sure we are doing no harm in the first round of refactoring. The :nav was the legacy way of storing navigation state, so events and subscriptions around it will eventually be deleted. Here's a more expansive example to complete this explanation:
(defn check-out-button [bill-entity till-id disabled?]
[:button
{:on-click #(dispatch [::at/across :bill 1
[[wc-till-events/check-out* [:phone bill-entity till-id] true]]]
#_[::wc-till-events/check-out :phone bill-entity till-id])
:disabled disabled?
:class (->class :phone/bottom-action-button)}
"Check Out"])
Just to tidy up, for the curious, s is the alias for Specter and ->class is what I use as the entry-point for ShadowCSS. And the true that you see here:
[wc-till-events/check-out* [:phone bill-entity till-id] true]
, is saying that check-out* takes app-state as its first arg - false would mean last arg. Lastly the second arg to the event ::at/across is a number. Here the number is 1 - which means that internally there's an assert to check that :at is already at the base of the tree.
For me every change of route needing to have a URL browser representation is too much to think about whilst building an application. Atrun is simple, designed for when your application lives inside a single URL. The :at path directly represents a way through your UI's tree structure without the translation layer of mapping URLs to routes and back again. You get simpler code, fewer concepts to learn, and no impedance mismatch between your navigation state and how you actually think about your interface hierarchy.
Atrun's design makes navigation events composable with arbitrary state mutations through the st->st function mechanism. This is great for real-world UIs where navigation rarely happens in isolation - you're usually also clearing selections, loading data, or updating multiple parts of state. With Atrun, these concerns flow together naturally in a single event dispatch. The result is navigation code that reads like what it does: "go down to the checkout screen, and also trigger the checkout process with these parameters."
Is Atrun more of a navigator than a router? Sure, but it is still all the router you most likely need. And when browser history and shareable URLs become requirements, Atrun's path-based design should make it easy enough to add bi-directional synchronization between the :at vector and the URL bar - but only where it is needed.
Published: 2025-12-07
Tagged: Re-frame ClojureScript