polylith

Andrew Leverette 2025-01-19T14:13:17.515349Z

So, I've run into a situation I'm unsure how to handle. Let's say I have the following components: task, database, and in-memory. I know that I can use configurable dependency to determine which persistence component to use, but how should I handle that in the task component. Should the task component implement adapters inside itself, or should I have two separate task components one for database interactions and another for in-memory interactions? I'm leaning towards the adapters, but I'm not sure if that violates the Polylith pattern.

seancorfield 2025-01-19T17:18:01.505599Z

The only "pattern" in Polylith is that bricks use components via their interface only. Are the database and in-memory components alternatives with a common interface? Is the choice between them made at build-time or should it be available at run-time? If it's a built-time choice and you can have a common interface for both implementations, then profiles and projects deps.edn making the selection is the right way to go (within Polylith).

Andrew Leverette 2025-01-19T18:14:00.955779Z

Is the common interface defined in the task component? For example, maybe define a protocol called TaskRepository and then define the adapters for the sql interaction and the in-memory interaction. Or do you mean define a common Repository interface?

Andrew Leverette 2025-01-19T18:15:20.476879Z

It resembles the ports and adapters architecture, but I might be misunderstanding something.

seancorfield 2025-01-19T18:42:15.025219Z

The way you spoke about the (theoretical) components, it sounded like database and in-memory were alternate implementations of the same thing -- a persistence interface.

Andrew Leverette 2025-01-19T18:44:17.632769Z

For the database and the in-memory components, my thought was that they would be lifecycle-managed components to manage either the state of the database connection or an atom that wraps a map.

seancorfield 2025-01-19T18:45:32.250299Z

An example from work: we have an i18n component connected to a database and an implementation of it that uses JSON as the data format. The API is identical: we have an i18n component and an i18n-preview component -- both have ws.i18n.interface as their (identical) API, with different implementations. The database-backed component is in the :+default alias (profile) and the JSON-backed component is in the :+preview alias (profile).

seancorfield 2025-01-19T18:45:55.688339Z

Do you need to be able to choose at runtime? Or build-time? That's the key decision.

Andrew Leverette 2025-01-19T18:46:16.476609Z

I think the choice would be at build time.

Andrew Leverette 2025-01-19T18:46:53.364019Z

Basically, what I'm think is that I should be able to swap the database component for the in-memory component and the task component shouldn't have to change.

seancorfield 2025-01-19T18:46:57.553059Z

When we build most of our projects, we select the database-backed i18n component. But our preview project selects the JSON-backed i18n-preview component. All the code uses ws.i18n.interface so it can't tell whether the i18n stuff is in the database or the JSON file.

seancorfield 2025-01-19T18:47:22.224959Z

Have you read the "profiles" stuff in the Polylith docs?

Andrew Leverette 2025-01-19T18:47:48.127759Z

No, I haven't got to that point yet.

seancorfield 2025-01-19T18:48:06.230179Z

https://cljdoc.org/d/polylith/clj-poly/0.2.22-SNAPSHOT/doc/profile explains how to have alternate implementations, selected at build time.

Andrew Leverette 2025-01-19T18:48:40.160689Z

So, in your example, are i18n and i18n-preview separate components that have the same interface?

seancorfield 2025-01-19T18:48:45.128569Z

Yes.

seancorfield 2025-01-19T18:51:34.511919Z

They both have src/ws/i18n/interface.clj and it's identical for both the i18n component and the i18n-preview component in terms of the same functions with the same signatures. But the implementations are very different (although, right now, those are both ws.i18n.impl -- they could have different names since the implementation ns is irrelevant to calling code).

Andrew Leverette 2025-01-19T18:56:09.604549Z

Hmm, is that interface defined in two separate places, or is there a single interface and the components are defined separately? I might be missing something.

seancorfield 2025-01-19T18:56:44.832189Z

There is literally src/ws/i18n/interface.clj in both components.

Andrew Leverette 2025-01-19T18:58:16.696059Z

Ah, okay. That makes sense now.

Andrew Leverette 2025-01-19T18:59:17.242389Z

So in my case there would be a task-sql component and a task-in-memory component?

seancorfield 2025-01-19T18:59:23.595489Z

All the other code requires and uses ws.i18n.interface. The projects deps.edn file select either the database-backed version (`i18n`) or the JSON-backed version (`i18n-preview`). The :dev alias does not contain either. :+default contains the one I use for development (the database-backed one) but there's a :+preview alias if I want to develop against that instead.

Andrew Leverette 2025-01-19T19:00:15.546889Z

Or something like that and they would have an identical interface.

seancorfield 2025-01-19T19:00:23.690189Z

You could have a task-sql component with <top-ns>.task.interface and a task-in-memory component with <top-ns>.task.interface

seancorfield 2025-01-19T19:00:53.734649Z

The fns/signatures would be identical. The implementations would be different (and the dependencies -- libraries -- would be different too).

seancorfield 2025-01-19T19:02:21.873279Z

Here's the profiles in our deps.edn (workspace-level):

:+default {:extra-deps {;; by default, we use the real i18n and web server:
                          poly/i18n        {:local/root "components/i18n"}
                          poly/web-server  {:local/root "components/web-server"}}
             :extra-paths ["components/i18n/test"
                           "components/web-server/test"]}

  :+preview {:extra-deps {;; or the preview i18n and the real web server:
                          poly/i18n        {:local/root "components/i18n-preview"}
                          poly/web-server  {:local/root "components/web-server"}}
             :extra-paths ["components/i18n-preview/test"
                           "components/web-server/test"]}

seancorfield 2025-01-19T19:03:00.415769Z

(we used to have a dummy web server for another context, hence the duplication above)

seancorfield 2025-01-19T19:03:57.002549Z

This is the preview version of ws.i18n.interface

seancorfield 2025-01-19T19:05:15.582139Z

This is projects/preview/deps.edn

seancorfield 2025-01-19T19:06:18.804959Z

All the other projects use :local/root "../../components/i18n"

seancorfield 2025-01-19T19:06:46.956189Z

Hopefully, that example helps?

Andrew Leverette 2025-01-19T19:51:37.536479Z

Yeah this is super helpful. So if the interface needs to change, then both component interfaces need to change.

seancorfield 2025-01-19T20:06:15.571299Z

Right. One common interface across all implementation components.

Andrew Leverette 2025-01-19T21:01:35.368709Z

I'm reading through the profile section now, and I've also read through the FAQ on the Polylith page which covers some of these questions, too -> https://polylith.gitbook.io/polylith/conclusion/faq