For those of you following domain driven design pa...
# random
t
For those of you following domain driven design patterns - have you ever considered using GQL as the domain interface given its ability to do fairly complex things? I'm noticing my domain and GQL api tend to be 1:1 for the most part and the pure domain layer is starting to feel like boilerplate. A lot become simplified if you collapse them into one
k
I would say the reason for the separation is that your domain layer will always be there even if you change the interface, if you combine these two you are making them aware of each other and your domain logic will be driven by the interface protocol. I know there is a lot of boilerplate that would feel like it is adding complexity but I think the moment you want to add a new way of interacting with your domain or want to remove GQL (for any reason ) this separation will be handy . One way to overcome the boilerplate for me is to create generators that generate code for the mapping.
g
At a previous place for the longest time we used JSONRPC because of this - the mapping for that was trivial 😄
t
Well I guess what I'm saying is even if I change the interface it could call into GQL
GQL is pretty expressive in terms of expressing args and return types, it basically looks exactly like defining a simple function
At least with the tooling I'm using
g
Yeah GQL is an object grpah. You could arguably write classes and have their properties and methods map into graphql fields
additionally methods or getters can return instances of other classes
t
Here's an example of pothos code
Copy code
createCustomer: t.field({
     type: CustomerType,
     args: {
       customerID: t.arg.id(),
     },
     async resolve(_, args) {
       return await Customer.create(args.customerID || ulid())
     },
   }),
I'm starting to wonder why I'm defining an additional Customer.create function. I've been doing it so far for the seperation angle that @Kujtim Hoxha mentioned but GQL is basically as simple as defining functions
I'm too scared to stop, feel like if I don't keep it separate it'll bite me one day but I'm unable to think of what I lose
I would just need a way for resolvers to call each other without going through http which feels simple
Then you can handle permissions entirely at gql level, use directives to hide some parts of the graph as internal
g
From our experience with JSONRPC, the only things that can actually bite you are things provided by the request context
t
tell me more
g
i wish i could but i think the CDN broke our docs syntax highlighting plugin 😄 we never fully open sourced the framework but services basically looked like this https://hfour.github.io/h4bff/
essentially we had classes class MyService implements IMyRPCServiceInterface { methods go here }
services were instantiated once per request
you could have services call into other services - at the end they all had to return JSON
to get another service you would just use
this.getService(OtherService)
now, there were things provided by the request context: i.e. current user JWT info etc. We made sure to wrap those in separate service classes
t
When services called each other did they go through http or just skipped that
g
they skipped it
additionally, we added decorators which defined permission checks
t
nice, yeah this general approach is basically what I'm imagining
g
and then it was up to you how to split them up 😄
you can keep them in the same process
or put them in a different one
you could also test them relatively easily by injecting a different provider for getService
you could either test them in isolation of any other services (full unit)
or only isolate certain HTTP-dependant things (mock request context)
or DB things, depends on what you like to test
the key item there was simple and flexible dependency injection 😄
it would be cool to see this kind of thing for GraphQL
where you also get the ability to return instances of other classes
k
I think the one thing you need to make sure if you use the GQL methods instead of a centralized method for these kinds of things is to always call the resolvers from other resolvers, the reason for that is that you might have other things that need to happen when you create a consumer (e.x send an event) you don't want that to be duplicated all over the place, the good thing about having it separated is that you don't have to fight a framework to do simple stuff e.x if you have a separate method that is agnostic to your protocol you can create additional tooling that just call normal methods e.x you could create a CMD client that creates consumers, in your case you would have to somehow cal the GQL resolver, to me this feels like forcing something into something that is not meant for that😅
g
and you can expose that entire object graph as a GQL endpoint
i haven't seen GQL systems that are geared towards doing this though
admittedly i haven't looked in much depth
t
Yeah @Kujtim Hoxha that's definitely right, the missing piece is skipping http when calling mutations/queries from within the same process in a type safe way. With the way pothos works though I feel like this can be as simple as a function call
I'd still have some functions that are "core" like
Bus.publish
k
If that is the case maybe it would work, the only draw back later would be if you for some reason stop using GQL and still have to maintain this just because everything is based around it, I suppose you could than migrate it to simple functions
t
Yeah refactoring it out feels like it could even be automated since pothos is so structured
k
That might be good than.. the other thing you might need to think about is validation if you rely on GQL validation and the code does not do it if you refactor it out you will have to add the validation somehow
t
Pothos integrates with Zod so you can define validation right there. I'm currently wrapping all my domain functions with Zod
k
If you can abstract the GQL resolver call in a way that it feels like just a function I would say this should be good and not a lot of drawbacks
t
I'm still too scared to commit to this though, gonna sit on it for a bit. Seeing @Gjorgji Kjosev have experience with a similar approach gives me some confidence though
g
we did go out of our way to try and not depend on any framework describing our service objects though. we ended up with our own framework 😅
k
I personally am a fan of separating but I also agree there is too much boilerplate code, i have been inspired for a lot of my work by https://github.com/Sairyss/domain-driven-hexagon, it is a lot of code but the codebase feels clean and has not started to feel like legacy code 😂
Usually, after a couple of months, it starts feeling like it needs refactoring… but it is not the case with these patterns at least for our team.
t
Yeah it minimizes how much code needs to change when you change your mind which is the #1 thing most codebases suck at
I found hexagon later but it's pretty close to what I'm already doing
k
100%, plus if you need to add functionality it helps you do that without touching any existing code, I found that CQS + Domain/Integration Events are just awesome to extend your app without changing existing implementation and you don’t worry about braking stuff that much
t
I've been considering publishing an event after any domain function executes with the function name+args passed in
k
That sounds very extendable, you would probably need to somehow handle the case when a function name changes…. I use classes and use MyClass.name for publishing/subscribing so even if the class of the event changes it will work 😂
t
Ah true
k
You could use the constants for function names 😂 {[MyFuncs.Name]: ()=>….} and then use MyFuncs.Name when listening…
I don’t love this + bye bye type safety…