Since the dawn of distributed computing, we've gone through many different mechanisms and standards for programmatically invoking remotely hosted functionality. CORBA, SOAP based Web Services and REST have all played an important role in this evolution. The more recent entrant into this mix is GraphQL, a technology I've had the chance to play around with lately.
A pattern I've noticed with the evolution of the IT industry involves "throwing the baby with the bath water". While SOAP based web services was feature rich and promoted a well defined and formal API pattern, it was considered too heavy, with XML based schema definitions and payloads as well as complex WS standards which needed to be supported by vendors and developer tooling. REST was meant to be lightweight, more efficient and flexible. REST reduced how we "see" a system into resources and four verbs: GET, POST, PUT and DELETE, an artifact of the decision to implement REST over HTTP. Instead of fixing just the challenges, REST introduced an entirely new paradigm that was very closely coupled to HTTP. While REST solved some of the complexity challenges with SOAP by focussing on HTTP and JSON, we also lost a few important features: the ability to have a self-explanatory and self-documenting schema and also the flexibility to think in terms of well defined "types" and "operations", which is undoubtably a more natural way to think about a remote system's capabilities. Some subsequent standards/solutions such as Swagger and OpenAPI mitigated some of these issues but not all.
To me, REST was an example of the "impedance mismatch" that can occur when we try to map a domain (in this example the "definition of a service or capability") from its most natural form into an adjacent paradigm that does not support all the same semantics. Another popular example of such an impedance mismatch is the mapping between object and relational paradigms, commonly referred to as the object-relational impedance mismatch. An inevitable consequence of any impedance mismatch is the additional complexity it introduces that has nothing to do with the end outcome but a consequence of the impedance mismatch itself. This fact should be self evident whether you are trying to map object inheritance to relational tables using one of many different strategies each with their own trade-offs or trying to force-fit a set of natural system capabilities or a service definition into resources and HTTP verbs.
When I started looking at GraphQL, I immediately fell in love with it as it felt like what REST should have been in the first place. Following are the key advantages that struck me as game-changing:
Following example:
#------------begin GraphQL definition----------------
# Domain Types
type Book {
# Unique ID for the book
id: ID!
title: String!
isbn: String
# Author of the book - notice that this is a reference to the Author type
author: Author
}
type Author {
id: ID!
name: String!
# List of books written by the author
books: [Book]
}
#Queries, Mutations and Subscriptions - for some reason they are considered special types
type Query {
# Retrieves all books
books: [Book]
# Retrieves an author by ID
author(id: ID!): Author
}
type Mutation {
# Adds a new author
addAuthor(name: String!): Author
# Adds a new book
addBook(title: String!, authorId: ID!): Book
}
type Subscription {
# Notifies when a new book is added - clients can subscribe to be notified of these events
bookAdded: Book
}
#--------------end GraphQL definition---------------
You might notice that this is simply a service contract and has nothing to do with implementation details. If you find yourself donning your JPA or ORM hat and asking how the books field of the Author type and the author field of the Book type are linked without a reverse reference, you've lost the plot!
2. The API user gets to specify what sub-graph it needs
While the API shown above can define the logical return (or response) type, graphQL allows (actually requires) the API client to specify exactly which fields of that type should be returned, including related entities and their fields, with no limit on the depth it can refer to. Following is an example of a query sent by the client:
query {
books {
id
title
isbn
author {
id
name
}
}
}
- One of the challenges with both Web Services and REST is that the response from an API call is fixed. So for Mobile vs Desktop vs Third Party Integrators, you may end up defining different APIs since the payloads would be different. This reality is captured in the Backend For Frontend (BFF) architecture pattern. With GraphQL, the client can specify the exact subset of the response that it needs, so you don't need multiple APIs for different clients. This is a significant simplification both in the definition and implementation of APIs.
- A second challenge of REST APIs is the chattiness and this is especially true for REST services exposed to third parties (which may be one of the BFF's mentioned above). In order to define the APIs to be reusable across many use-cases, designers have no choice but to resort to a high level of granularity. So if you were an outside integrator connecting to the above service, you might end up sending multiple REST calls to the books resource and the author resource, resulting in a high level of chattiness. Can you overcome this? Yes, by including authors in your books response, but this may involve accessing and returning unnecessary amount of data for those usecases which don't need them. Can you avoid this again? Yes by having two different resource definitions for books with authors and books without authors. You can see how the so called "solutions" quickly become a series of complex decisions and trade-offs introducing unnecessary amount of complexity to the design and implementation of the APIs!
- Another obvious benefit of this is the resilience of the API to change but I've captured this in the separate versioning section below
3. A built in playground/UI to explore and invoke the API
While the GraphQL schema is human readable, graphQL includes a UI called GraphiQL that can be used in the development environment to explore and invoke the API. This can be a powerful tool to improve collaboration and reduce the back and forth conversations between the front-end and backend-engineers as well as across different teams accessing the API. Even before designing and implementing their user interfaces, front-end engineers can explore and test the backend APIs they need using this interface: A very powerful collaboration mechanism for the team.
4. Provides for federation - the benefits of Microservices with the convenience of a Monolith!
All of us have a good understanding of the advantages of the Microservices paradigm compared to a Monolithic service so I'm not going to get into that. One of the downsides of Microservices compared to a Monolith is the complexity of orchestrating across microservices and navigating relationships that span multiple microservices. Say you have a products microservice and inventory microservice. The inventory microservice needs to return a response that includes product details. There are multiple ways of achieving this but with different trade-offs and no seemingly ideal solution. GraphQL enables a smart solution to this problem in the form of GraphQL federation. This allows GraphQL schemas to externalize some of the relationships without worrying about which GraphQL microservice will be the provider for that reference. A proxy is responsible for combining the schemas from these disparate microservices and exposing a "super-graph" to clients, an extremely elegant solution to the problem! Finally the elusive "best of both worlds" type of solution where you get all the benefits of separately deployable microservices with monolith like simplicity for the clients.
5. No versioning required! GraphQL APIs are much more resilient to change
In GraphQL, the client specifies exactly what fields of the response entity and related entities it needs. When the schema evolves with additional fields, API provider does not need to introduce new versions and the client results does not need to modify its code, even though the underlying types could have changed form. So GraphQL eliminates the need for versioning and facilitates API evolution without the massive overhead associated with REST or even SOAP web services. It also supports deprecation in case fields need to be dropped, so there can be a smooth transition of clients to a new version of the schema without having to maintain multiple versions.
I would be remiss if I didn't mention some of the drawbacks of GraphQL:
- GraphQL has not reached the level of popularity of REST especially for external/third-party integrations so many organizations end up having to expose their GraphQL APIs as REST endpoints to their external clients.
- REST allows security to be configured through URI filtering which is very simple to understand and implement even through proxies. In GraphQL, since the client can specify which fields it needs security needs to be handled both at the operation (query/mutation/subscription) level as well as the schema level. This needs to be facilitated in the implementation and cannot be done through a proxy. However, with the right effort to create a framework to facilitate this, it can be as simple as adding annotations to the operations and types/fields.
- Caching in REST is simple as it can be simply a matter of caching the results against the URL + parameters. GraphQL caching however is complex as different API calls can retrieve different parts of the same sub-graph as the client has the flexibility to request only what it needs. Client frameworks (eg. Apollo client and Relay framework) has solved this problem by maintaining the cache in a relational manner.
Some important frameworks and references:
Apollo GraphQL server (NodeJS)
DGS framework for Springboot (supports federation)