How to organize GraphQL code into packs and handle relationships between private models in different packs?

This message was imported from the Ruby/Rails Modularity Slack server. Find more info in the import thread.

Message originally sent by slack user U7213XMGS3H

Hello! I have a question about organizing GraphQL code into packs. In most graphql-ruby implementations I’ve seen there’s a coupling of GraphQL types and ActiveRecord models. This poses a problem because ideally while modularizing our code into packs, we would make model classes private. GraphQL by it’s nature needs to describe relationships between models that are private in various packs.

Let’s imagine we had 2 packs, customers and orders.

We might imagine a GraphQL type in the orders pack

# packs/orders/app/public/graphql/types/order_type.rb

class Types::OrderType < GraphQL::Schema::Object
  field :some_field, String

  field :customer, Types::Customer
end

In a unmodularized monolith, I would assume there would be a belongs_to relationship between the Order and Customer models. This would mean that when graphql-ruby called order.customer it would run a database query. (We’d probably want some batching in place, but perhaps that’s out of scope for this conversation.)

If my models are private in two separate packs, how can I achieve this? Do I:

• Make graphql-ruby represent plain ruby objects and not models? Have public APIs from the packs return these objects? I like this route, but it seems extremely hard to achieve incrementally because I already have hundreds of types defined that relate to models
• Make models public? This feels quite wrong, and one of the reasons I’m so interested in Packwerk is that I want ActiveRecord to be used only in private
• Put each model (or sets of models) in their own packs that are public, but is only imported into certain places. For example, a GraphQL batch loader pack could import all models. Feels kind of harebrained, but it could work.

Hey <@U7213XMGS3H>! I’m definitely no expert in graphql, so I’m hoping some folks with more expertise than me can chime in.

At Gusto, we’ve definitely found something similar to you – there is a tradeoff between what GraphQL wants and what we want for our modularized app.
graphql-ruby wants ActiveRecord, our other ideals push us to not expose AR as a public interface
graphql has cycles as a feature – packwerk rejects cycles as a feature
graphql exposes its own “interface” to systems that may or may not use the existing (or not) interface of a system
The most important bit I want to mention is that this tradeoff is definitely present still as we’re figuring out the best way to reconcile these technologies, and I’d encourage any team to set up their graphql/packwerk stack in a way that creates the minimum friction and maximum value for engineers.

That being said, at Gusto, here are some things we do to help mitigate this friction:
• Although we originally put each domain’s graphql stuff in the pack for that domain, we reversed this decision and decided to merge graphql concerns in their own pack. This has a couple of nice advantages:
◦ The issue of cycles is all but eliminated, since the graphql pack can have cycles within itself, but should still not have cycles with other packs.
◦ Generally speaking, it encourages the graphql pack to use the public interface of other packs. There are ways for graphql to call plain old Ruby APIs instead of AR APIs, which we try to encourage. This also pushes us to take GraphQL concerns (e.g. pagination) and find a way to bake them into our plain ruby APIs, which increases reusability of those ruby APIs. That being said – folks skip this and use private implementation anyways, which is fine too! This can be done incrementally too (you don’t need to migrate everything at once).
• Another option is to add graphql paths to the exclude key of packwerk.yml . That way you allow packwerk to add value to non-graphql code and you let graphql do its thing unencumbered.
Over time I Hope graphql-ruby can find ways to be as convenient for POROs as ARs, e.g. by expecting an interface to data rather than a specific implementation, but that’ll be a community effort where we feel the most pain.

Message originally sent by slack user U7213XMGS3H

Thanks @AlexEvanczuk, this is a great answer. It’s good to hear I’m not missing some obvious pattern that would perfectly reconcile these issues.

Our problem is that we have hundreds of current GraphQL types that are mostly tied 1:1 to AR models. We want to move these AR models into packs privately. Many GraphQL fields map to model methods or relations. I’m struggling to imagine how we can do this incrementally.

Another solution I started working up (but ultimately abandoned) was to use an in process graphql gateway. I got this idea by asking myself “how would we handle graphql if we were splitting up the code into different services.” The issue with this strategy is that there’s only one gem I can find for schema stitching, and it’s pretty new and doesn’t have a lot of adoption. I tried to get it to work with our large schema and it failed in inscrutable ways. I thought about spinning up an out-of-process gateway and hitting multiple GraphQL controllers (1 for each pack) but that seemed kind of crazy.

Given that Shopify has a large GraphQL API, I’m curious if they’ve developed any patterns… any Shopify devs reading this?

Message originally sent by slack user U7213XMGS3H

Another thought I had was to make a public API exclusively for graphql that returned AR objects, but then enforce that with a rubocop rule.