One of the main benefits of having everything behind a single endpoint is that data can be routed more effectively than if each request had its own service. While this is the often touted value of GraphQL, a reduction in complexity and service creep, the resultant data structure also allows data ownership to be extremely well defined, and clearly delineated.
This is in large part because the request itself is pointed towards a single endpoint, and that request must package the expected data format and the resource owner designation, whether it be for a specific function or a specific data type, is thus intrinsically tied to that request.
This is different from a typical REST API, in which the request to each microservice might be requesting data that the microservice endpoint doesn’t support or cannot deliver, which would then be passed on to another endpoint, and so on. This ends up creating a web of requests and passed communication, and to the average viewer, knowing who actually owns the requested resource becomes hard to decipher.
Another benefit of adopting GraphQL is the fact that you can fundamentally assert greater control over the data loading process. Because the process for data loaders goes into its own endpoint, you can either honor the request partially, fully, or with caveats, and thereby control in an extremely granular way how data is transferred.
In many ways, this granularity is what REST has tried to achieve with some degree of success when implementing specific schemas and request forms. That being said, GraphQL integrates this as a specific structural element of how it works, and as such, does so more effectively than most other solutions.
Because a single GraphQL request can be composed of many requested resources, and because GraphQL can choose dynamically when, how, and to what extent a response is issued to such a request bundle, GraphQL can leverage a sort of parallel execution for resource requests. What this functionally results in is the ability for GraphQL requests to partially fail, but for the response to deliver more useful data to more requests in a single issuance.
This ultimately has the benefit of allowing a single request, even when partially failed, to serve the place of what would traditionally be multiple requests to multiple services over multiple servers. This also allows a single request to use a relatively more restrained amount of processor time and power to deliver the same amount of information that would otherwise be required of those multiple requests, thereby delivering greater power with less requirements.
“Instead of having six sequential calls, what [a parallel dataloader] will do is give you all six IDs in one go, so you can make one request instead of six. This will make your downstream databases and APIs way happier – no one will be on fire, no one getting paid at 2 am in the morning.” -Tomer Elmalem
In GraphQL, requests can be given a “maximum execution time”, and this value can then be used to budget requests. Each request is given a value, and from that, the server budget is calculated and calls are prioritized. As an example, let’s assume our server has a total budget of 2 seconds, and look at a sample batch of requests.
We receive four requests – one is a single second, two of them half a second, and the final request a full two seconds. When budgeting our requests, the GraphQL server can accept the first three requests, and either delay or refuse the last request, as it would exceed the allotted time that the server has open for requests of its nature.
What this in effect does is allow the server to prioritize requests and grant them where appropriate, which ends up reducing timed out requests in the long run. Additionally, this system can return information to the requester – such as would be the case with a 6 second request, for instance – that can then inform the user to break their requests into smaller pieces or wait for a low-budget time to make the request.
Combining two of these major benefits – parallel requests and budgeting – allows us to more effectively plan out our query schedules. By being able to send some queries to an endpoint, and have others execute in parallel at a later time per their weight and processor demand, you can effectively plan out those queries over a relatively broad set of criteria and per the time allotted.
While this seems simple, the ability to plan out queries over time and address per priority is one of the elements of GraphQL that is so incredibly powerful.
GraphQL utilizes Object Identifiers for one of the biggest savers in terms of server processing demands – caching. By being able to cache resource for services which request them, GraphQL can essentially build a cache of often-requested data and save processor time and effort.
According to Elmalen, this is rather easy to implement, in fact – GraphQL documentation suggests utilizing a system of globally unique IDs in order to note the resources being cached, thereby providing them with minimal processing demand.
GraphQL is unique, in that, in a normal connection and request cycle, there is no real “fail” or “succeed” – everything is either succeed or partially fail. GraphQL requests can partially succeed, returning only elements of the data requested or specific datasets as allowed, and this control can be very granular.
As a result of this, a resolver to a request can be more than “all or nothing,” partially resolving as a single resolver delivers content, and the rest of the request is dumped. This means that GraphQL handles failure rather gracefully, notifying the requester of the failure but returning useful, actionable data alongside the noted failure and what caused said failure.
As part of this, GraphQL requests can be parsed to find the null fields returning in a failed request, and note what the error actually was. If the error was a simple misconfiguration or poorly formed call, it can be made in a better format upon instruction from the GraphQL layer itself – if the data is prohibited or absent, the requester can also ascertain this. This means that automatic retries are possible, as are granular levels of call termination and auto-failure handling.