architecture

orestis 2021-01-26T07:51:19.001600Z

I have an architecture question about the interaction of the component library and web frameworks like ring, pedestal etc. Usually web requests will need access to pretty much every component under the sun, on top of request-specific information (like who the user is etc). How is this to be modelled without having to pass in the entire system under a key in the request map?

orestis 2021-01-26T09:26:42.003300Z

We do the same with component — subsystems with dependencies, and it’s working well. I’m more asking about “don’t pass the system around” which is a common advice…

👍 1
Jivago Alves 2021-01-26T09:54:17.003500Z

Same here. For example, our web server component usually has dependencies on db and monitoring components only.

lukas.rychtecky 2021-01-26T11:43:51.003800Z

Or maybe decompose a service layer into components and pass only needed dependencies per a service (component). It’s similar as you do in Spring etc. But I don’t know if this approach would be better/readable.

orestis 2021-01-26T07:52:49.003Z

In our current code we’re creating a map with namespaced keys that correspond to various components that the requests need, and we pull those out in our handlers. But as gradually add more and more components, we have to expose those too, so it eventually degenerates to be a renamed version of our main component system.

vemv 2021-01-26T16:55:05.004Z

> Usually web requests will need access to pretty much every component under the sun This strikes me as the root cause. It doesn't seem normal to me that every handler needs every component. That could mean any of these two: * there's a somewhat excessive amount of low-level components * maybe: create a higher-level component for them * or: don't create as many components - sometimes direct access can be OK * the app intends to be highly modularized, but it breaks its own module boundaries * this happens quite often which trying to use somehing like the Polylith but without any automatic means of ensuring that modules have a tractable dependency graph

lukas.rychtecky 2021-01-26T07:56:07.003100Z

With Integrant you can decompose a system into components and define dependencies for each component. A component could be a subset of routes.

orestis 2021-01-26T17:08:55.005800Z

I see some interesting points here, which leads me to ask: how many components do your apps have? It would fun to see the results of (keys system) to see what kind of dimensions people use...

orestis 2021-01-26T17:11:21.008600Z

In our case we have roughly a dozen components (afk so I can’t run that code atm) - and our web server component needs: • the multi tenant support • The mongo connection factory • The Postgres connection factory • The solr instance • The notification component • The email component • Some config stuff

orestis 2021-01-26T17:12:06.009600Z

So some real low level stuff isn’t used from the web (eg we have some queue component that isn’t exposed)

athomasoriginal 2021-01-26T17:26:20.012800Z

I’m using integrant, but aside from that my components look like: • postgres connection • app config • firebase system • stripe system However, I will need to add my own email and other components as I go, so I would imagine it to include similar ones to what you have. The stripe and firebase ones are components because they require a “state object” which throws warnings/errors if you try to restart the whole system so I manage their lifecycles via integrant.

orestis 2021-01-26T17:31:39.013700Z

And I guess you have to pass all these to the web component, right?

athomasoriginal 2021-01-26T17:46:06.015200Z

I have to pass postgres and app config to my HTTP Handler component (web component), yes.

athomasoriginal 2021-01-26T17:46:43.015900Z

And then as I add things, I would likely have to pass those as well, so I can see where your coming from, but haven’t reached it as of yet.

athomasoriginal 2021-01-26T17:51:55.020100Z

> I’m more asking about “don’t pass the system around” which is a common advice… That question was the interesting one to me as well. I would like to see an example of an alternative approach. I figured what I do is fine (for now) because it’s not a map with random things that I pass in, but a curated map….which maybe is the same thing, but with more discipline? 😆

lukasz 2021-01-26T17:53:37.022500Z

Does integrant not have dependency settings like Component?

lukasz 2021-01-26T17:54:03.023400Z

As in, I can define which parts of the overall system my component will need in the system definition

athomasoriginal 2021-01-26T17:54:31.024100Z

Yes, it does (if i’m following your meaning).

lukasz 2021-01-26T17:54:45.024500Z

of course, it's possible to have a component (say web handler), which require everything - but you never pass the whole system explicitly

phronmophobic 2021-01-26T17:55:37.025500Z

having a bunch of keys in a map that you don’t care about isn’t an issue. it can, but doesn’t necessarily lead to some other problems like: • since every request may interact with any key, reasoning about the system becomes strained • producing information for the request that may not be needed may introduce noticeable overhead • others? It might be useful to further clarify the problem.

phronmophobic 2021-01-26T17:59:37.029Z

Another underlying problem might be that requests are strongly coupled to specific implementations of resources they need. For example, rather than just requiring some necessary user info, they are instead interacting with a mongo connection.

➕ 1
athomasoriginal 2021-01-26T18:03:28.031800Z

This is a good example of a problem I see a lot in the wild.

Jivago Alves 2021-01-26T18:04:22.032600Z

If I remember correctly, we have 31 components in our System. But the web server (and all remaining components) only depends on the DB and Monitoring for now. I think there’s nothing you can do if there’s a dependency between things. You can re-think if you really need a dependency. Our project is doing mostly data pipelines so they are more or less independent from each other. We are now breaking them into different “services”.

Jivago Alves 2021-01-26T18:06:04.033Z

Yes, we use protocols for “interfaces” that are mature and we know every component is using them to avoid spreading internal details.

Jivago Alves 2021-01-26T18:08:10.033200Z

However, we still have some details for some things that are not very well mature yet. We prefer to feel the pain first and then refactor later. For that reason, we have integration tests which cover the interaction between components.

➕ 1
seancorfield 2021-01-26T18:24:25.037200Z

We have an "application" component which is reused across most of our web apps (which also have a "web server" component and a few others). That "application" component contains database (multiple connection pools), environment, caches, ElasticSearch, Redis, template engine (wrapper around Selmer that installs tags and filters at startup), and "SDKs" for communication with some of our subprojects, plus a few other things. Then there are a handful of other components that are used in some apps but not others (e.g., Microsoft Active Directory, email-based error logging, "presence" -- for tracking online members on our sites).

seancorfield 2021-01-26T18:25:45.038200Z

We do not try to create interfaces/protocols/APIs that hide all the implementation details except where we genuinely have multiple implementations.

2021-01-26T18:31:56.039800Z

we kind of lean on existing interfaces/protocols, some of our components implement clojure.lang.IFn, so you can look up a configuration value by calling the configuration component as a function

2021-01-26T18:32:41.040200Z

presence implements a few protocols from core.async

2021-01-26T18:33:52.042Z

the nice thing about re-using interfaces and protocols is you can get mocks/stubs in tests "for free", so like if we need to mock the configuration component in a test for some reason, because it acts like a function, you can just use a function

💯 1
seancorfield 2021-01-26T18:34:10.042600Z

Yeah, implementing IFn on a component is a nice affordance -- and work is where I got the idea to do that for next.jdbc.connection/component 🙂

lukasz 2021-01-26T18:34:26.042900Z

Interesting that we never run into 'too many dependencies passed in' problem in our applications - perhaps it's because we run a SOA?

orestis 2021-01-26T19:28:08.044200Z

So I guess it’s the application component that in the end acts as the “orchestrator” and gets everything else as a dependency I guess, right @seancorfield ?

orestis 2021-01-26T19:29:07.045300Z

I’m curious to see how component is used without records/protocols, is it just a map with dependencies then?

seancorfield 2021-01-26T19:29:46.046100Z

@orestis Several apps have that application component as a subcomponent, but that's where most of our system dependencies live because that's our "core" system across most of our apps.

seancorfield 2021-01-26T19:30:44.047200Z

When I said no "interfaces/protocols/APIs", I meant we don't write those to wrap subsystems -- which you see as examples sometimes of "how to use Component". I'm not talking about the two lifecycle methods of Component itself.

seancorfield 2021-01-26T19:31:13.047900Z

That said, we do have non-record implementations of Component's lifecycle. And next.jdbc works that way too.

orestis 2021-01-26T19:31:59.050Z

My original question came from an argument at work - why have a protocol and a record where you could just have a function?

seancorfield 2021-01-26T19:32:28.051200Z

So it's an empty hash map with a start function and then a function with a stop function attached.

orestis 2021-01-26T19:32:41.051600Z

Especially in the case of non-stateful components where you just need the dependencies

seancorfield 2021-01-26T19:32:49.052Z

Component requires associativity for things with dependencies.

seancorfield 2021-01-26T19:34:49.055200Z

I asked Stuart Sierra about enhancing Component to run dependencies via metadata and he felt it was too narrow a need (because only a few things can carry metadata that aren't already associative). So, if you need dependencies, you need a hash map or a record (but you don't need the protocol implementation if the component has no lifecycle). And if you don't need dependencies, you can have anything that carries metadata.

seancorfield 2021-01-26T19:35:46.056300Z

See https://github.com/stuartsierra/component/issues/67 for his response.

orestis 2021-01-26T19:36:56.059Z

To rephrase: most protocols have “this” as their first argument. Is there any point in making a protocol + implementation (reify or record) vs just defining a “public” function that takes “this” as a first argument where “this” is expected to be a hash map with dependencies?

2021-01-26T19:36:58.059100Z

one way to think of component is has a system for building a graph of closures

orestis 2021-01-26T19:37:46.060200Z

The caller shouldn’t care because to the caller the dependencies are opaque - record or hashmap makes little difference

2021-01-26T19:38:03.060500Z

for protocols it is usually to support multiple implementations

orestis 2021-01-26T19:39:19.063100Z

Right, so if you don’t care for that, you don’t really need protocols. Tests can always redef the public function if they need to stub out stuff.

2021-01-26T19:40:02.063800Z

they can, but redef is fairly brittle

2021-01-26T19:40:57.064900Z

a def is a global thing, so is a redef, so if you have two instances of a component, redefing the public function to behave differently for one of them becomes a chore

orestis 2021-01-26T19:41:51.066100Z

That’s true, but then you are back at square one if you want testability, right? A protocol or multimethod...

orestis 2021-01-26T19:42:02.066600Z

Or that IFn trick which sounds interesting

2021-01-26T19:42:10.066800Z

multimethods implementations are also global

2021-01-26T19:43:07.067900Z

at my last job some people really hated records and protocols, so we had what was basically a fork of component where the lifecycle protocol was replaced with multimethods

2021-01-26T19:44:22.069300Z

it is ok, but you can't just (reify ...) up something that those multimethods will do the right thing with in the middle of a test if you need to stub/mock

orestis 2021-01-26T19:46:29.072400Z

It’s just annoying that re-evaluating protocols breaks existing implementations and you have to reload everything.

2021-01-26T19:47:43.073800Z

our protocols are pretty all defined in a namespace like *.protocols which pretty much only contain protocols and almost never change (don't need re-evaluating)

2021-01-26T19:50:01.075500Z

if you use multimethods, I recommend doing something similar, pull the defmultis (the interface definition) into a distinct file

seancorfield 2021-01-26T19:50:20.076Z

(I wish I'd followed that pattern more rigidly with next.jdbc -- I mistakenly put a few protocols in with other code!)

seancorfield 2021-01-26T19:51:01.077100Z

I do like the idea of a Component-variant that uses just functions (with metadata for dependencies as well as lifecycle hooks).

2021-01-26T19:51:06.077300Z

no always, but often enough mixing implementations and interfaces leads to pain

orestis 2021-01-26T19:58:04.080200Z

My takeaway from this is that if you want to be able to stub out things protocols are needed. Put them in another file. See if you can implement IFn then the protocol is just a function which can easily be stubbed.

orestis 2021-01-26T19:58:53.082200Z

Perhaps if a record cannot be avoided, defer all the logic in a plain function and implement the protocol by just forwarding the calls to the plain function, this way you can REPL away without having to tear down your system.

2021-01-26T19:59:47.082800Z

"plain function" is doing a lot of hand wavy work there

2021-01-26T20:21:26.083200Z

alternative is :extend-via-metadata

2021-01-26T20:22:15.083700Z

Oh you said they hated records and protocols.

2021-01-26T20:22:22.083900Z

Well, you can get rid of the record bit.

orestis 2021-01-26T20:32:58.085200Z

ClojureScript does it this way I think, a lot of protocols that just forward the call to a similarly named function...

orestis 2021-01-27T08:09:31.093800Z

Oh, I stand corrected. I wonder why’s the case; perhaps a GCC optimisation?

orestis 2021-01-26T20:33:46.086300Z

I don’t hate anything 😅 just trying to figure out stuff, without cargo culting some approaches blindly.

orestis 2021-01-26T20:37:45.087700Z

About the IFn approach, is that where your protocol would have a single function so it’s replaced by a record that implements IFn?

seancorfield 2021-01-26T21:01:06.088200Z

It doesn't need to be "a record that implements IFn" -- it can just be a function.

seancorfield 2021-01-26T21:01:52.088800Z

It only needs to be a record if you have dependencies and you want it to implement IFn as well.

orestis 2021-01-26T21:04:43.091600Z

Right, I meant that to the caller the component is a function. If it’s a plain one or an IFn record or a closure is an implementation detail...

orestis 2021-01-26T21:05:18.092500Z

I’m off to bed, I’ll revisit all this tomorrow in front of a computer. Thanks for a nice chat!

2021-01-26T21:07:47.092600Z

clojurescript does the reverse of this, a lot of functions that forward the call on to a protocol function