At Gusto, there has been a big push to think about modularization at every step of the development process. Whether it’s driving features in the monolith through the use of packwerk, or adopting yarn workspaces in our front-end code, modularization has been a key initiative to help scale feature development at Gusto.
The decision to adopt GraphQL as the communication protocol between the frontend and backend was made in order to harness its inherent advantages. GraphQL offers a significant degree of freedom and flexibility, enabling developers to craft requests tailored to specific client use cases, ultimately enhancing the efficiency and precision of data retrieval. However, as feature development progressed, this newfound flexibility led to an unforeseen challenge: our codebase evolved into a monolithic structure encompassing various team domains. Within this vast GraphQL schema, diverse objects with fields from different teams coexisted, and the accompanying extensive test files became a source of friction for developers, hindering their ability to seamlessly integrate new features.
Let’s dive in with an example to highlight the pain point. One of the object types that is part of our graph is the Company model. There are several fields tied to this root object, such as payroll related fields, state tax registrations, and benefits related fields.
module Objects class Company < BaseObject description 'A company' field :can_run_payroll, Boolean, null: false do description 'If true, the company has no blockers/setup preventing it from running payroll' end field :state_tax_registrations, ::StateTaxRegistrations::Graphql::Objects::CompanyStateTaxRegistrations, 'The state tax registrations for this company', null: false field :has_benefits, Boolean, null: false do description 'If the company has Gusto-managed benefits, or is in the process of signing up for them' end end end
Wait a minute here! Fields appear to belong to various domains.
When a developer wishes to extend this object, they typically add a new field and resolver to the main file and then also introduce a related test in the spec file. This process can result in both the object and test files becoming overly large and difficult to manage as teams enhance the functionality of the object to meet new feature requirements.
GraphQL subgraphs (or "federated schemas") allow for clear domain boundaries, enhancing scalability and reducing the complexity associated with managing large codebases. This technique involves dividing a single GraphQL schema (a “supergraph”) into multiple "subgraphs" which can be developed, deployed, and scaled independently.
The result? A streamlined development process, improved scalability, and increased team autonomy. From the client’s perspective, the interface looks exactly the same, while the backend can now be split up into the different domains, allowing teams to manage their own subgraph, all while following the same practices as other teams.
Rails Generators: Automating the Workflow
“If it’s not automated it’s not important” - Unknown
As the idea of subgraphs started to proliferate, there were several questions that developers had to ask themselves when setting up a subgraph.
- What should I name by subgraph?
- Where should my subgraph be located?
- How should my subgraph interact with service and model layers?
- How should I name my classes?
- How can I extend existing types to add fields?
- How should I test my subgraph?
- What commands do I need to run to have my subgraph up and running?
As the codebase started moving towards a modularized architecture, we want to ensure the engineers are focusing on delivering value and not boilerplate setup that adds overhead to the entire process. That's where Rails generators come into play. In a Ruby on Rails context, generators are scripts that create boilerplate code to speed up your development process. They can create everything from models and controllers to complete authentication systems.
We developed a custom Rails generator to help automate and standardize the creation of subgraphs. With this generator, we can create a new subgraph which follows best practices for organizing schema and test files. Let’s see how it works.
First, the generator asks for the name of the subgraph so that a new pack can be created. This step lays the groundwork for the location of subgraph specific files.
Next, the generator asks for a description of the subgraph and creates or modified some files to get the subgraph up and running, including:
- graph_q_l_controller.rb: Responsible for routing requests to your subgraph
- subgraph_schema.rb: Responsible for declaration of objects, queries, and mutation types, as well as description and optional middleware
- base/field.rb: Represents the base field class that your subgraph will use for all object fields in the subgraph
- base/object.rb: Represents the base object class that your subgraph will use for all object types in the subgraph
- routes.rb: Contains the route entries for the new subgraph
- graphql_service_list.json: Contains configuration for all subgraphs in the repository
After all prompts have been answered, the generator will create all of the files necessary to scaffold a new subgraph. Here's what that looks like:
Improving the Developer Workflow
After introducing the Rails generator, nine subgraphs have been created ranging from payroll to benefits to other domains. The automation of directory structure and naming conventions eliminates room for error and inconsistency, making our codebase easier to navigate and maintain. It also reduces onboarding time for new developers, who can easily understand the project structure and start contributing quickly.
Subgraphs and Rails generators combined offer a compelling strategy to manage large-scale applications efficiently. By breaking down a monolithic schema into manageable, domain-specific subgraphs, we significantly improve our scalability and team autonomy. When we automate the creation of these subgraphs with a Rails generator, we ensure consistent best practices, while allowing teams to focus on building features for our users, instead of spending time setting up boilerplate.
So, if you are a Rails developer working with GraphQL, consider integrating these two powerful tools into your workflow. The initial setup might take some time, but the long-term benefits in code quality, developer productivity, and application scalability are well worth the investment.
To a more modular and healthy codebase. Cheers!
Want more GraphQL goodies? Gusto maintains a library that supports Apollo Federation. Check it out here https://github.com/Gusto/apollo-federation-ruby