polylith

onetom 2026-06-08T07:58:28.220429Z

the https://polylith.gitbook.io/polylith/conclusion/faq has this question: > What's the point of an interface if you can't (or can you? how?) swap out the implementation? but it doesn't expicitely answer the (or can you? how?) part. it just says > ... then we “swap” (at compile time) to the user-remote component ... based on this example: https://cljdoc.org/d/polylith/clj-poly/0.3.32/doc/profile#add-profiles-to-dev-deps

▾ workspace
  ▾ components
    ▾ user
        interface.clj
        core.clj
    ▾ user-remote
        interface.clj
        core.clj
it seems i can't have 2 different implementation of a component present at the same time within a https://polylith.gitbook.io/polylith/architecture/2.4.-development JVM process. i think that would be highly desirable, because one could be used as the oracle implementation for the other, both at the time of the initial implementation and later too, when we are trying to keep their behaviors in sync. how people solve this problem? rename 1 implementation temporarily? or somehow it's not a problem in practice, because one can fire up multiple dev project REPLs, with slightly different configs, like in the example, -A:dev:test:build:+default and -A:dev:test:build:+remote?

imre 2026-06-08T09:22:12.224889Z

> fire up multiple dev project REPLs, with slightly different configs this is how it's handled in polylith

onetom 2026-06-08T09:56:13.705129Z

that largely falsifies the benefits promised in the https://polylith.gitbook.io/polylith/architecture/2.4.-development section of the official docs: > The development project gives us a delightful development experience that allows us to work with all our code from one place in a single REPL it seems we can't run all automated tests from that single REPL process, because tests need fake implementations of the various interfaces, but real and fake implementations should map to the same x.y.z.interface NS, so they can't co-exist within the same clojure process, without shadowing each other 😕 in a project, which needs Datomic Local, Apache POI and similarly heavy dependencies, the boot time for test runner processes is a significant overhead, compared to the test runs themselves, turns the development experience into NOT-delightful.

imre 2026-06-08T10:01:20.316979Z

what would delightful dev experience look like for you in this case?

onetom 2026-06-08T10:15:36.735919Z

run the tests from the single dev REPL with kaocha.repl/run that's how we are doing it now. every test has their own throw-away, in-memory db, fake http api clients, which are programmed to return answers based on an expected state

imre 2026-06-08T10:46:25.600109Z

and do you have both the fake and the real implementations on the classpath for these test runs?e

onetom 2026-06-08T10:59:32.552769Z

correct. the decision between using the real and fake implementation happens at the creation phase of a stuart-sierra-like-system-map, which we call a service or svc, for short. we use https://github.com/aroemers/rmap - which is just a regular clojure map - to declaratively describe the components and their dependencies for a svc instance. then many of our domain-logic functions receive a svc instance as their 1st argument and look up components by their fully qualified keyword keys. it's roughly what's described in https://functionalbytes.nl/clojure/rmap/2020/07/04/rmap-2-update.html

imre 2026-06-08T11:00:31.031449Z

so you're emplying runtime polymorphism to ini this or that resource

onetom 2026-06-08T11:01:09.267119Z

yes, by having a map key lookup as the mechanism for the polymorphism

imre 2026-06-08T11:01:17.790759Z

polylith's "different implementations" value-add is classpath-time polymorphism so you either have this or that implementation on the classpath

imre 2026-06-08T11:01:58.078469Z

but the two can coexist and polylith doesn't force the use of either, it just becomes a tool to be used in certain situations

onetom 2026-06-08T11:04:46.169139Z

sure, but then the extra .interface NS structure is mostly noise and can't help with ensuring compatibility between "real" and "fake" implementations. either that, or we can't enjoy the purported "delightful single REPL DX"...

onetom 2026-06-08T11:10:08.314209Z

there is 1 benefit of having explicit .interface NSes. AI coding agents don't have to waste tokens on reading the entirety of implementations. but the cost is having to maintain the function signatures consistency between the sole implementation and its interface namespace... i think this kind of token saving is better solved by a custom agent skill / tool, which can construct the equivalent of .interface files on the fly, using clj-rewrite or a tree-sitter query, but i haven't tried implementing this yet.

onetom 2026-06-08T11:12:19.059919Z

that's an other question, whether they actually save tokens and not read both the interface and the implementation namespace, wasting even more tokens... i haven't conducted experiments yet, so not sure, whether that's a problem or not. it probably highly depends on the LLM and system prompt.

onetom 2026-06-12T03:43:27.912019Z

and then compts/facts-DEV/src/omni.facts.ifc would depend on both compts/facts-{FILE,RAMA}/src/omni.facts-{FILE,RAMA}.ifc while compts/facts-PROD/src/omni.facts.ifc only on one, e.g. compts/facts-RAMA/src/omni.facts-RAMA.ifc? (capitalization is just for emphasizing the relevant parts)

onetom 2026-06-12T06:08:08.788949Z

i just came across this section in the clj-poly tool's doc: https://cljdoc.org/d/polylith/clj-poly/0.3.32/doc/profile#_verifying_our_work it has a tip, shown to be Cursive specific, but in-fact it's about how to use a single REPL process and still switch between implementations. not great, since tests - running from the dev REPL too - might still need to switch between fake and real implementations on a per-test basis, at runtime, but at least during purely manual - single actor - REPL driven development, there is a way to avoid a REPL process restart with a different poly profile.

imre 2026-06-12T09:03:57.129479Z

> and then compts/facts-DEV/src/omni.facts.ifc would depend on [...] This is pretty much how I thought about it. Maybe with a little variation: compts/facts-DEV/src/omni.facts.ifc -> compts/facts-DEV/src/omni.facts (implementation) -> both compts/facts-{FILE,RAMA}/src/omni.facts-{FILE,RAMA}.ifc