polylith

Mark Wardle 2024-07-01T20:04:02.173699Z

Hi all. Really enjoying migrating a fairly complex project to polylith and the whole process is making it so much more obvious where things had become less modular and more coupled. So thank you for making this available. I'm currently using integrant - I can see whether using component, or integrant, or any other state / system management library is pretty orthogonal to the polylith repository layout and dynamic classpaths based on projects, but it feels as if the integrant defmethod for a key is a bit less obvious than a simple function creating a 'service' component, and perhaps with polylith I'm leaning more towards separating configuration from integrant defining system architecture and injecting dependencies. Component seems as if it is a better fit, but then I have to create defrecords for everything, or extend the Lifecycle protocol to any existing records/types in dependent libraries. What is the recommended approach? Has anyone used integrant with polylith - it is working [I'm trying not the change too much as I port).

👀 1
1
Patrix 2024-07-05T04:47:00.469169Z

Good discussion! I had indeed followed @mark354’s approach of just putting my defmethods in each component. I don't mind being tightly coupled to integrant though in this application.. But I do find it a bit messy that the init-key's are all over the place. Might look into this approach of putting them in bases. However this runs the risk of duplicate keys (if 2 bases use the same components for example)? thinking-face

👍 1
tengstrand 2024-07-05T06:16:27.211869Z

@mstoyukhin Has made an https://cljdoc.org/d/polylith/clj-poly/0.2.20-SNAPSHOT/doc/example-systems#integrant-system for Integrant, that you can have a look at.

👍 1
🙏 1
Mark Sto 2024-07-05T09:00:55.517719Z

However this runs the risk of duplicate keys (if 2 bases use the same components for example)?@patrix Normally, you will not get into such naming collision if you follow the Integrant’s approach for naming components (system “keys”) with namespace-qualified keywords and distributing your Integrant components between separate namespaces accordingly. Example: • workspace sets the :top-namespace to be "glob.uniq" • we have bases alpha and omega • each of them uses a component called fred • for the sake of simplicity, each declares a dedicated top-level ns for fred • we end up with namespaces glob.uniq.alpha.fred and glob.uniq.omega.fred • following the Integrant’s convention for keys (see load-namespaces fn docstring), we name them :glob.uniq.alpha/fred and :glob.uniq.omega/fred, respectively Voila! Now you can mix and match these two freds within Integrant systems of your workspace however you like. It should also be noted that if it is necessary to use 2+ instances of a component within the same base/system, the problem is solved by using unique name aliases. Following the example above, fred can be aliased e.g. as busy-fred and lazy-fred, which are defined in namespaces with corresponding names. But that’s just one approach among many.

Patrix 2024-07-05T09:01:54.974709Z

You’re right. I meant duplicate defmethods

imre 2024-07-05T09:02:22.084359Z

you can also use composite keys

👍 1
Patrix 2024-07-05T09:02:58.032039Z

I guess I would need to not require multiple bases from my development project user.clj to avoid this.

imre 2024-07-05T09:03:14.278309Z

there's one more thing you can use

imre 2024-07-05T09:04:28.152469Z

given component fred you can define functions there and leave it integrant-free, and create an integrant-fred component (or fred-integrant) which just does the defmethods over fred's interface

imre 2024-07-05T09:04:59.547579Z

can then use integrant-fred with multiple bases if needed

imre 2024-07-05T09:05:11.916419Z

(and still swap the fred implementation underneath)

imre 2024-07-05T09:06:05.291149Z

sure it's double the components but isn't this what poly is for?

Patrix 2024-07-05T09:07:14.944609Z

Right. Might be better. Let’s say I have two projects/bases that both use the same component. I’d have to define the init-key method in both but it would need to be the same method defined twice. A -integrant component that pairs with the actual would prevent that and prevent any issue of loading both bases at the same time in my development project.

Patrix 2024-07-05T09:08:06.005799Z

And would provide a clear indication to humans working on the code where to find the init-key methods

Mark Sto 2024-07-05T09:08:10.630379Z

@imre Yes, this is a legit approach too. Still, I’d prefer to leave a single system-agnostic component implementation for fred and keep all parts of the actual system inside a particular base. That way we won’t run into the issue mentioned by @seancorfield, when we end up with multi-methods spread across entire codebase. (This all depends on the size of the codebase/workspace though, but can be tough on larger ones.)

imre 2024-07-05T09:09:07.304539Z

well, the approach I suggested would keep all ig multimethods in components having integrant in their names

imre 2024-07-05T09:09:26.620379Z

so it's still quite restricted yet reusable

👍 1
imre 2024-07-05T09:10:29.220699Z

and allows room to use different lifecycle management libs between projects if you so wish - mount-fred clip-fred etc

🤔 1
Patrix 2024-07-05T09:13:20.385519Z

@mstoyukhin what I’m thinking with your approach is in the development project, if you require two bases that both use the Fred component and both define an init-key method with the same name …. That seems like it would cause a problem. Both in having duplicate methods of the same name and in having to keep in sync both implementations of the methods. Solved IMP by @imre ‘s approach but I’ll have to experiment and find out!

Mark Sto 2024-07-05T09:16:48.293939Z

> both define an init-key method with the same name Which I never suggested. All system keys must be unique, ideally globally unique.

Mark Sto 2024-07-05T09:19:30.774849Z

having to keep in sync both implementations of the methodsWhile this can be true, there are situations when you need to have them different (not in-sync), e.g. when different bases provide different dependencies/input parameters for fred’s instantiation. It all matters and depends on the context (of a particular project). DRY is not an absolute.

Patrix 2024-07-05T09:19:59.906689Z

That’s also an interesting point

😉 1
Patrix 2024-07-05T09:20:26.016539Z

So far I’m just imagining/extrapolating from my own code base

Patrix 2024-07-05T09:20:35.869509Z

While walking lol

Mark Sto 2024-07-05T09:26:33.553939Z

Yeah, that’s ok. In fact, I had this very situation — with two Integrant bases using (almost) the same set of shared components within the workspace — on a real commercial project. The above approach could solve the issue of running both in a single Integrant system, which is a usual case with development project. (Though we ended up never actually doing so, because, what for? These bases were mutually exclusive, since the second one was a replacement artifact for the first one.)

Patrix 2024-07-05T09:46:30.902349Z

in either case, what stands out is: separate the component (stateless/implementation) from its stateful elements (integrant init-keys/component lifecycles), whether that's in other components or directly within the bases, depending on how the projects are structured. in my case, a lot of statefulness is the same between projects or bases (e.g. web server, db connection, db migrations, etc), so the -integrant components approach would make sense to me.

seancorfield 2024-07-05T17:12:42.236759Z

Interesting discussion. I hadn't thought about the ig keys needing to be globally unique -- that's not a problem with Component as you can build arbitrary systems from shared components and have multiple instances of Components in a single system easily, because the "program" names everything when it assembles the system.

imre 2024-07-05T18:02:05.875519Z

They only need to be unique within the system you're launching

imre 2024-07-05T18:02:12.410659Z

(ig keys)

imre 2024-07-05T18:04:05.108159Z

In a config map you can use composite keys to launch multiple instances of something

imre 2024-07-05T18:05:26.597249Z

Wrt multimethods the keys there should still be unique within a classpath I suppose. But that's just like that with namespaces. Our ig keys match pretty well with their containing nses so we aren't seeing collisi9ns

👍 1
Mark Wardle 2024-07-10T17:22:08.850139Z

Thanks all. In fact, my problem with integrant was less about polylith, and more that I was essentially passing my whole 'system' to many 'components'. The fix, of course, was to include only local dependencies in a local environment. So once I sorted that, having a polylith component define integrant multimethods seems fine, and trying to only use integrant in a base, and not as the definition of the component 'state' wouldn't work. I've now merged my polylith experimental branch into my 'main' as I much prefer the polylith approach to modularising my code. I'm porting a Java (Apple WebObjects) application currently, and think the polylith approach will make ongoing work much easier. So thank you!

👍 1
1
✅ 1
Mark Wardle 2024-07-10T17:22:25.818289Z

My current code: https://github.com/wardle/pc4

Mark Wardle 2024-07-10T17:23:14.399749Z

Some of my polylith components are simply wrappers around external libraries, because that's what I had before... so perhaps not a very good example sorry!

Mark Wardle 2024-07-10T17:25:28.619049Z

My integrant configuration is derived from https://github.com/wardle/pc4/blob/main/components/config/resources/config/config.edn and I think this will need to be workspace-wide.... but obviously different bases might conceivably choose different keys to load.

Patrix 2024-07-11T02:05:18.108319Z

nice! I've also gone with an approach as discussed before, where now the multimethods are defined in separate components.. It's a bit cleaner though with an extra level of indirection. However that extra level helps avoiding searching all over the codebase for where those methods are defined, just look for *-integrant components.

👍 1
Mark Wardle 2024-07-01T20:05:00.636039Z

Current status :)

tengstrand 2024-07-01T20:11:33.632429Z

Happy to hear that you have had a good experience with Polylith so far! My advice is that you search for "in:#polylith integrant" because this has been answered earlier by users that use Integrant. Hope that helps.

👍 1
Mark Wardle 2024-07-01T20:16:22.061849Z

Thank you! Missed those posts. I'll have a look.

👍 1
seancorfield 2024-07-01T20:28:08.044109Z

I'll also note that Component does not require a bunch of defrecords -- you can attach start/stop implementations to things as metadata. You only need an associative thing (hash map or record) if you want dependency analysis. If you need dependencies but no start/stop, use a plain hash map. If you need start/stop but no dependencies, use anything that carries metadata (`next.jdbc` uses a (fn ...), for example). If you want your Components to implement other protocols etc., use a record -- we often implement IFn on our Components at work to return some specific part of the state.

👍 1
Mark Wardle 2024-07-01T20:35:43.215189Z

That’s interesting. Thanks! I am quite attracted to the Component approach in which I can have a plain function in my polylith interface - the multimethod approach in integrant has been excellent but perhaps one abstraction more than is needed with polylith - and with polylith I can see I might want a master config file but wiring up the runtime ‘system’ is more a ‘function’ of a Polylith base in code and not config. But obviously it can be done in integrant too, but it’s aesthetics. I will experiment! Thanks.

seancorfield 2024-07-01T20:58:06.073599Z

I personally prefer Component over Integrant. While I appreciate the somewhat more "data-first" approach of Integrant, I feel it loses out on "simplicity" as it has several more extension points than Component's start/`stop` and I find scattering defmethod all over the code really makes it difficult to get a handle on what exactly is being executed (I dislike defmethod in general and use it very sparingly, and nearly always with all implementations in one file).

👍 1
Teemu Kaukoranta 2024-07-02T05:43:50.540479Z

Mark, you could also consider not using Integrant/Component etc in your polylith components. If your polylith component has a lifecycle, you can for example just make a record out of it. The polylith base can then manage the lifecycle using Integrant / Component whatever.

👍 1
Mark Sto 2024-07-02T05:47:26.091369Z

@mark354, hey! As a co-author of a Polylith repo that launches quite a few systems (with different approaches/libs), I find that Integrant makes a perfect fit with it. The only thing that I’d keep in mind is that it makes sense to split your components (“keys” in Integrant terms) into two parts: 1. the init / halt! implementation functions go to a particular component (that can be used with any library, in fact, e.g. Component, Integrant, Donut Party System, Mount, etc.), 2. while the Integrant-specific multi-methods go to a particular base (the actual application or service) that is itself a system. This way you’ll be able to maintain an approach/library-independent abstract system component lifecycle-aware Polylith bricks.

👍 1
👍🏻 1
👍🏼 1
👀 1
☝️ 1
Mark Wardle 2024-07-02T06:39:29.800839Z

This all makes a lot of sense. As I have migrated I’ve just copied in the multimethods into my polylith components but it felt incorrect to do so. Moving the orchestration to the base makes sense to me and reduces impact of any specific decision between component vs integrant vs anything else

💯 1
Mark Wardle 2024-07-02T06:39:34.246599Z

Thanks all.