Apollo Graphql at Enterprise Scale Final
Apollo Graphql at Enterprise Scale Final
Apollo Graphql at Enterprise Scale Final
A Principled Approach to
Consolidating a Data Graph
GraphQL at Enterprise Scale
A Principled Approach to Consolidating a Data Graph
Jeff Hampton
Michael Watson
Mandi Wise
GraphQL at Enterprise Scale
Copyright © 2020 Apollo Graph, Inc.
Published by Apollo Graph, Inc.
https://www.apollographql.com/
All rights reserved. No part of this book may be reproduced in any form on by
an electronic or mechanical means, including information storage and retrieval
systems, without permission in writing from the publisher. You may copy and
use this document for your internal, reference purposes. You may modify this
document for your internal, reference purposes
This document is provided “as-is”. Information and views expressed in this
document may change without notice. While the advice and information in this
document is believed to be true and accurate at the date of publication, the
publisher and the authors assume no legal responsibility for errors or omissions,
or for damages resulting from the use of the information contained herein.
Revision History for the First Edition
2020-09-11: First Release
2020-10-27: Second Release
2020-12-10: Third Release
Contents
The Team v
Preface vi
Who Should Read this Guide . . . . . . . . . . . . . . . . . . . . . . vi
What You’ll Learn from this Guide . . . . . . . . . . . . . . . . . . . vii
How to Contact Us . . . . . . . . . . . . . . . . . . . . . . . . . . . vii
iii
iv Contents
v
Preface
The data graph has quickly established itself as an essential layer of the mod-
ern application development stack. In tandem, GraphQL has become the de
facto technology for managing this new layer with its enticing promise to bring
together all of an organization’s app data and services coherently in one place.
And thanks to the wellspring of experimentation and innovation with GraphQL
over the years, it has proven itself a mature and capable technology that’s ready
for scalability.
GraphQL makes its way into an enterprise’s tech stack through a variety of
avenues, for instance, a single team eager to leverage its client-driven approach
to data fetching. However, as its adoption spreads realizing GraphQL’s promise
at scale requires coordination and consolidation of these efforts across teams.
At Apollo, we’ve had the opportunity to work with countless developers in a
wide range of enterprises over the years. Through that work, we’ve learned
that a unified, federated data graph is at the heart of any successful GraphQL
consolidation project. We first shared some of these insights in Principled
GraphQL where we outlined best practices that organizations can follow to
create, maintain, and operate a data graph as effectively as possible. In this
guide, we’ll provide a detailed road map for putting these principles into action
at the enterprise level.
vi
What You’ll Learn from this Guide vii
This guide is for business leaders. Consolidating your data graph isn’t just
about the architecture of your tech stack. It’s about an organizational transfor-
mation that will harness the power of graphs to unlock platform value. A unified
data graph increasingly lives at the center of value delivery in an enterprise and
the strategies and tactics presented in this guide provide a pathway to realizing
the potential of your data-graph-as-a-product.
This guide is for developers and architects. Whether you’re a developer on a
client team or actively maintaining a GraphQL server in production now, the
concepts outlined in this guide will give you a clearer understanding of how
your work can align to your organization’s broader GraphQL strategy and even
become a “Graph Champion” on your team.
How to Contact Us
We’d like to hear from you if you have questions about this guide or have a
unique perspective you’d like share about using GraphQL your organization.
Email us at solutions@apollographql.com to reach out at any time with
your comments or if you require any assistance implementing GraphQL in an
enterprise environment.
Moving Toward GraphQL
Consolidation
1
2 Moving Toward GraphQL Consolidation
clients (for example, web and iOS) to interact with a monolithic, general-purpose
API. Alternatively, BFFs can save client applications from making requests to
multiple backend services to obtain all of the data required to render a particular
user interface view.
As a solution, BFFs add a new layer where each client has a dedicated BFF
service that directly receives the client’s requests and is tightly coupled to that
user experience. For teams creating BFF services, GraphQL can be a natural fit
for building out this intermediary, client-focused layer and adopting this pattern
can be an important first step toward consolidating a data graph.
In practice, the BFF pattern with GraphQL may look like this:
Client-Only GraphQL
Teams that opt for client-only GraphQL approaches are motivated to improve
their client development experience by layering GraphQL on top of the REST
endpoints or other legacy APIs they have to work with. And while improved de-
veloper experience is a win, beneath this abstraction the client application will
still incur performance costs as it maintains responsibility for making multiple
requests to various services to gather all of the data required to render a view.
BFFs
Like client-only approaches, teams that use GraphQL with BFFs enjoy the advan-
tage of improved developer experience by way of a consumer-friendly GraphQL
API, but they also manage to overcome the performance issues incurred by
client-only approaches. BFFs accomplish this by providing a unified interface for
a client application to send its requests while also handling the heavy lifting of
querying multiple backend services on behalf of the client.
However, there is an inherent tradeoff in building and maintaining BFFs. When
every client team is empowered to create a BFF to suit their needs, there will be
inevitable duplication of effort across those teams. However, where BFFs are
shared between seemingly similar clients in an effort to reduce duplication, then
the GraphQL schema contained within can balloon in size and become confusing
due to the lack of clear ownership.
Monoliths
The pains that emerge from shared BFFs are only sharpened with monolithic
GraphQL server implementations that have shared ownership. Portions of a
graph may be well-designed to suit the needs of certain client teams only, while
other clients must find workarounds or create overlapping types for their own
use. Correspondingly, standardization becomes an issue because the shape of
the graph evolves myopically on a client-by-client or a feature-by-feature basis.
Even in scenarios where a dedicated server team maintains ownership of the
graph challenges quickly arise when more than one graph definition is required
for a single product in order to support the needs of multiple clients. A server
team may also find itself burdened with the task of building and maintaining the
necessary tooling to evolve the schema over time to meet new product needs
without breaking compatibility for any clients that are actively consuming data
from the graph.
6 Moving Toward GraphQL Consolidation
Apollo Server provides open source libraries that allow it to act both as
an implementing service and as a gateway, but these components can be
implemented in any language and framework. Specifically, Apollo Server
supports federation via two open-source extension libraries:
• @apollo/federation provides primitives that your implementing
services use to make their individual GraphQL schemas composable
• @apollo/gateway enables you to set up an instance of Apollo
Server as a gateway that distributes incoming GraphQL operations
across one or more implementing services
We will cover consolidated GraphQL architectures using Apollo Federation
and Apollo Gateway in-depth in Chapter 3.
Incremental Adoption
If you currently use a monolithic GraphQL server, then you can break its func-
tionality out one service at a time. If you currently use a different architecture
like schema stitching, then you can add federation support to your existing
implementing services one at a time. In both of these cases, all of your clients
will continue to work throughout your incremental migration. In fact, clients
have no way to distinguish between these different data graph implementations.
Separation of Concerns
Federation encourages a design principle called separation of concerns. This
enables different teams to work on different products and features within a
single data graph, without interfering with each other.
By contrast, traditional approaches to developing distributed GraphQL architec-
tures often lead to type-based separation when splitting that schema across
multiple services. While it may initially seem straightforward to divide a schema
by type, issues quickly arise because features (or concerns) managed by one
service often span across multiple types that are located in other services.
By instead referencing and extending types across services, concern-based
separation offers the best of both worlds: an implementation that keeps all
the code for a given feature in a single service and separated from unrelated
concerns, and a product-centric schema with rich types that reflects the natural
way an application developer would want to consume the graph.
When deciding to break a monolithic graph into a federated one or when expand-
ing a federated graph by adding new services and teams, an architect should
have a plan for addressing the above four areas of concern. The decision ma-
trix below is annotated with each of these concerns and provides guidance in
resolving any gaps in measuring, understanding, and addressing these concerns.
Summary
Consolidating GraphQL APIs across the enterprise can help bring a much-needed
measure of consistency to how this technology is implemented for both data
graph contributors and consumers alike. Moving toward a unified, federated
approach allows an organization to scale its GraphQL APIs, obtain new perspec-
tives on its data graph, reuse infrastructure, and enable teams to ship code
faster. When the time is right to move toward a consolidated data graph, enforc-
ing proper separation of concerns in the underlying services will allow teams
to continue to rapidly iterate while adhering to the constraints imposed by the
federated implementation.
In the next chapter, we’ll explore the topic of graph ownership within an organi-
zation as well as how to plan for the successful roll-out of a consolidated graph
architecture with federation.
Graph Champions in the
Enterprise
By Jeff Hampton
As we explored in the previous chapter, GraphQL adoption patterns can vary
considerably within large organizations. In some instances, GraphQL is identified
by architects and applied as an incremental pattern of API consolidation or
mediation. Alternatively, GraphQL spreads organically among product teams
looking to accelerate their delivery with the safety and support afforded by the
GraphQL specification and community. Regardless of its inception, GraphQL
adoption naturally grows beyond a single team’s ability to reason about what is
being developed in an enterprise.
Apollo’s experience has revealed a consistent need for a specific skill set around
GraphQL in an enterprise. To put it plainly—regardless of the investment
model—GraphQL adoption will eventually generate the need for consolida-
tion once two or more teams invest in a data graph. The enterprise’s Graph
Champions will be instrumental to this consolidation effort.
In this chapter, we’ll further explore the concept of the data-graph-as-a-
product, identify its customers, and explore the skills and products necessary to
consolidate GraphQL within an enterprise. We’ll then scope the responsibilities
of Graph Champions and their role in organizational excellence and we’ll explore
each component of graph championship and data graph administration with key
deliverables and approaches to address consolidation challenges.
15
16 Graph Champions in the Enterprise
Governance
Broad initiatives are best served by a team whose focus and value is well-
understood across business units and organizational boundaries:
• Graph Champions are recognized as a source of truth for GraphQL within
the organization
• With an increased altitude, Graph Champions can be entrusted with the
security of the graph and its access
• Teams can rely on Graph Champions to bring clarity to cross-cutting
concerns (for example “how do I reference an end-user?” or “how do
we handle media, currency, and internationalization consistently in our
products?”)
• Establish and maintain deprecation and long-term-support (LTS) sched-
ules based on end user and consumer demand for graph features
Health
Graph Champions support healthy, consolidated, and federated data graphs that
have these key characteristics:
• Healthy adoption of a single, federated graph requires rigor in maintain-
ing a cohesive, easy-to-consume graph surface
• Service discovery and product development depend on consistent
document documentation, style, and availability
• Consumers can serve end users quickly because the federated graph has
consistent naming and logical organization
The Graph Champion and Graph Administration 17
Advocate
Graph Champions serve the interests of multiple customers and stakeholders
through support and service by:
• Defending the role of the data graph to business leadership
• Providing education to new customers in the languages and parlance of
the teams to which they belong
• Onboarding and facilitating discussions, RFCs, and architectural reviews
Equip
Successful “digital transformation“ strategies often under-prioritize engineering
ergonomics and tooling. A successful Graph Champion equips each customer of
the data graph according to their needs by:
• Providing and manage tooling for other teams to use and evolve the graph
• Establish common, polyglot patterns and sound practices for effective
GraphQL use
• Supporting delivery systems, including integration, testing, artifact reg-
istries, and IDE tooling
• Builds products for End Users using existing and new graph features
• Are concerned with performance, new product development
3. Contributor
• Resolve graph data to underlying systems
• Fulfill product-driven requests from Consumers
• Collaborate with Consumers through tooling, education
4. Sponsor
• Enable CI/CD and provide delivery platform
• Maintain operational excellence
• “Last Mile” to the End User
With these personas in mind, we can further contextualize the key responsibili-
ties of Graph Champions from the previous section to gain a holistic view of their
role in supporting a consolidated data graph in an organization:
Summary
Graph Champions provide essential capabilities to an enterprise’s GraphQL
consolidation work. When viewed as a product, the value of a data graph serves
many technical customers and, ultimately, the business’ strategic goals. A
successful consolidation strategy needs leaders that can properly equip data
graph contributors with the tools they need while also advocating for, governing,
and maintaining the overall health of the data graph. Graph Champions are
also well-positioned to help an organization navigate some of the challenges of
consolidation while providing educational support to graph contributors and
consumers alike.
Consolidated Architectures with
Federation
By Mandi Wise
Chapter 1 touched on the high-level architecture of GraphQL APIs that are con-
solidated via federation. By embracing this federated approach, teams can
address the lack of consistency that often emerges from other non-federated
GraphQL architectures while also exposing data within the graph in a demand-
oriented way. In this chapter, we’ll explore federation’s various implementation
details and architectural considerations in greater depth to gain a better under-
standing of how to fully realize its benefits.
22
A Better Way to Scale Distributed GraphQL Architectures 23
Historically at Apollo, we have seen that federation usually isn’t a starting point
for most enterprises in the early stages of adopting GraphQL. While it can be in
some cases, implementing federation before running GraphQL in production
with a pre-consolidation pattern will likely necessitate large education and
integration efforts for the teams who will be responsible for managing portions
of the data graph. It may also skew the focus of this process heavily toward data
modelling across services instead of product delivery.
More often, as GraphQL’s surface area expands across teams’ tech stacks,
pain points emerge as these teams attempt to scale within the various pre-
consolidation patterns (discussed in Chapter 1) and perhaps even begin to
experiment with other non-federated approaches to consolidation. Graph Cham-
pions within the organization emerge and drive the teams toward a federated
architecture to unify the disparate portions of the data graph, increase developer
velocity, and scale GraphQL APIs more effectively.
In our experience, these paths are well-worn and converge on a shift toward a
federated data graph. This transition was designed to minimize disruption to
teams that are currently contributing to and consuming existing GraphQL APIs.
When this transition is properly executed, champions can improve the semantics
and expressiveness of the data graph while facilitating improved collaboration
between teams. Federated architectures achieve these ends by adhering to two
core principles: incremental adoption and separation of concerns.
Additionally, whatever portion of the data graph you scope into an initial im-
plementing service should have at least one client that actively continues to
consume this data. From the client’s perspective, the transition to federation can
and should be as seamless as possible, and continued consumption of this data
can help you validate assumptions, test out new federation tooling, and help
you understand how to best delineate future implementing services’ boundaries.
// ...
When the gateway starts up, it uses the URLs provided in the serviceList to
fetch the schema from each implementing service to compose the federated
data graph. In production, we recommend running the gateway in a managed
mode with Apollo Studio (using static configuration files instead of querying
service schemas at start-up), which we’ll explore further later in this chapter.
When a request reaches the gateway-enabled Apollo Server, it will execute the
incoming operation across the implementing services and then form the overall
response. How that request is optimized and fulfilled across the federated data
graph is determined by a key feature of the gateway known as query planning.
Connecting the Data Graph with Entities 27
At a high level, query planning works by optimizing for the most time spent in
a single service to reduce the number of network hops. More specifically, the
gateway used a service-based depth-first approach to operation execution
across services, unlike the breadth-first approach typically used by monolithic
GraphQL servers.
The @key directive defines a primary key for the entity and its fields argu-
ment will contain one or more of the type’s fields. For example:
The @key directive may be used to define multiple primary keys for an entity:
The @key directive also supports compound primary keys for nested fields:
type Organization {
id: ID!
}
Referencing Entities
After defining an entity in a schema, other implementing services can reference
that entity in their schemas. In order for the referencing service’s schema to be
valid, it must define a stub of the entity in its schema. For example, we can refer-
ence a Product type defined in one service as the return type corresponding to
a product field on a Review type defined in another service:
Note that the GraphQL spec-compliant extend keyword is used before the
referenced Product type, indicating that this type was defined in another
implementing service. The @key directive indicates that the reviews service will
be able to identify a product by its UPC value and therefore be able to connect to
a product based on its upc primary key field, but the reviews service does not
need to be aware of any other details about a given product. The @external
directive is required on the upc field in the Product definition in the review
service to indicate that the field originates in another service.
Because the reviews service only knows about a product’s UPC, it will be unable
to resolve all of a Product type’s fields. As a result, the reviews service’s resolver
for the product field will only a return a representation of the product with the
primary key field value as follows:
Connecting the Data Graph with Entities 29
{
Review: {
product(review) {
return { __typename: "Product", upc: review.upc };
}
}
}
Resolving References
To resolve any additional fields requested on Product, the gateway will pass
that representation to the products services to be fully resolved. To fetch the
product object that corresponds to the reference, the products service must
implement a reference resolver for the Product type:
{
Product: {
__resolveReference(reference) {
return fetchProductByUPC(reference.upc);
}
}
}
With these resolvers in the place, the gateway can now successfully coordi-
nate execution of operations across service boundaries and clients can make
GraphQL query requests to a single endpoint and in a shape that expresses the
natural relationship between products and reviews.
Extending Entities
Referencing entities is a key feature of federation, but it’s only half of the story.
While an entity will be owned by a single implementing service, other services
may wish to add additional fields to the entity’s type to provide a more holistic
representation of the entity in the data graph. Doing so is a simple as adding the
additional field to the extended type in a non-originating service. For example, a
reviews service’s schema may add a reviews field to the extended User type
that was originally defined in an accounts service:
The reviews service must then implement a resolver for the user’s reviews:
{
User: {
reviews(user) {
return fetchReviewsByUsername(user.username);
}
}
}
When extending entities, it’s important to keep in mind that the entity’s orig-
inating service will not be aware of the added fields. Additionally, each field
in an entity must only be defined once or the gateway will encounter schema
composition errors.
Multiple implementing services may also resolve a field when data has been
denormalized across those services. In this scenario, applying the @provides
directive on a field definition that returns an extended type will tell the gateway
that certain fields for that entity can be resolved by the extending service too:
As a result, Apollo Federation provides support for shared value types including
Scalars, Objects, Interfaces, Enums, Unions, and Inputs. When implementing ser-
vices share value types, then those types must be identical in name in contents,
otherwise, composition errors will occur.
Custom Directives
Apollo Gateway provides support for both type system directives and executable
directives. Type system directives are applied directly to an implementing
service’s schema while executable directives are applied in operations sent from
a client.
To provide support for type system directives, Apollo Gateway effectively ignores
them by removing all of their definitions and uses from the final composed
schema. The definitions and uses of these custom directives remain intact in the
implementing service’s schema and are processed at that level only.
Executable directives, on the other hand, are treated much like shared value
types. These directives must be defined in the schemas of all implementing
services with the same locations, arguments, and argument types, or else com-
position errors will occur. Correspondingly, implementing services should also
use the same logic to handling executable directives as well to avoid ambiguity
for the clients that apply those directives to operations.
Managed Federation
In the previous examples, we have seen how to run a federated data graph us-
ing a list of service URLs. As a best practice, Apollo Gateway can also run in a
managed federation mode and use Apollo Studio as the source of truth for each
implementing service’s schema. With managed federation, the gateway is no
longer responsible for fetching and composing schemas from the implement-
ing services. Instead, each service pushes its schema to a registry, and upon
composition, Apollo Studio updates a dedicated configuration file for the graph
in Google Cloud Services. The gateway then regularly polls Apollo Studio for
updates to the data graph’s configuration, as visualized below:
Reference Implementation
The majority of the code examples in this chapter were derived from the Acephei
example in the Apollo Server repository. You can find the complete source code
for the Acephei demo here.
You can also access an example of Acephei’s managed, federated data graph in
Apollo Studio at demo.apollo.dev.
34 Consolidated Architectures with Federation
Summary
In this chapter, we explored the features and benefits of a federated schema
and how they may be realized using Apollo libraries. Federation is underpinned
by the principles of incremental adoption and separation of concerns. By
adhering to these principles, teams within an enterprise can work toward a
consolidated GraphQL architecture along a minimally-disruptive migration path.
Federation enables teams to independently, yet collaboratively, manage por-
tions of the single, unified data graph. Entities are the key feature of a federated
data graph that provides the extension points among implementing services and
power that collaborative work.
With an understanding of the basic mechanics of federation in place, in the next
chapter, we’ll explore schema design best practices with special consideration
for federated data graphs.
Federated Schema Design Best
Practices
By Mandi Wise
GraphQL is a relatively new technology, but from its rapid and widespread adop-
tion has emerged a host of common schema design best practices—both from
the enterprises that use it at scale every day, as well as the broader developer
community. The majority of best practices that apply to non-federated GraphQL
schema design also apply when designing service schemas within a federated
data graph. However, federated schema design rewards some additional best
practices when extracting portions of a data graph into implementing services
and determining what extension points to expose between service boundaries.
As we saw in the previous chapter, entities are the core building blocks of a
federated data graph, so the adoption of any schema design best practice
must be approached with the unique role of entities in mind. A successful
federated schema design process should begin by thinking about what the initial
entity types will be and how they will be referenced, extended, and leveraged
throughout the graph to help preserve the separation of concerns between
services—both today and as the graph evolves in the future.
When migrating from a client-only or monolithic GraphQL pattern, that work
begins by identifying what entities will be exposed in the first implementing
service extracted from the larger schema. When migrating from an architecture
consisting of BFF-based GraphQL APIs or any other architecture of multiple
overlapping graphs, the work of identifying entities (and determining new
service boundaries, in general) may be a bit more complex and involve some
degree of negotiation with respect to type ownership, as well as a migration
process to help account for any breaking changes that may result for clients.
Whatever your architectural starting point, Apollo Federation was designed
to allow the work of identifying entities and defining implementing service
boundaries to be done in an incremental, non-disruptive fashion. Beginning to
35
36 Federated Schema Design Best Practices
identify these entities is also the essential prerequisite for adopting the other
schema design best practices that will follow.
In this chapter, we’ll explore some proven best practices for GraphQL schema de-
sign with a specific lens on how these practices relate to federated data graphs,
as well as any special considerations and trade-offs to keep in mind when design-
ing and evolving schemas across a distributed GraphQL architecture.
When thinking about driving product experiences via the data graph, keep
in mind that the overall schema of the data graph is a representation
of your product and each federated schema is the representation of a
domain boundary within the product. This is why Apollo Federation excels
at supporting omni-channel product strategies—the data graph can be
designed in a demand-oriented way that’s based on product functions
and the clients that query the graph can, in turn, evolve along with those
functions.
type Review {
id: ID!
productID: ID
}
With federation, however, a reviews service’s schema can represent a true subset
of the complete data graph:
type Review {
id: ID!
product: Product
}
The revised Mutation fields better describe what is happening from a client’s
perspective and offer a finer-grained approach to handling updates to a prod-
uct’s name and description values where those updates need to be handled
independently in a client application. Using two separate update mutations
also helps disambiguate what would happen if a client sent the patchProduct
mutation with no name or description arguments (because the mutation
could handle updating one value or the other, but does not require both for any
given operation) and saves the implementing service from having to handle
these errors at runtime. We’ll speak more on the use cases for finer-grained
mutations in the next section.
As a final, related point on hiding implementation details in the schema, we
should also avoid exposing fields in a schema that clients don’t have any reason
to use. If a schema is intentionally and iteratively developed based on the
aggregation of product functions and client use cases, then this issue can easily
be avoided.
However, when tools are used to auto-generate a GraphQL schema based on
backing data sources, then you will almost invariably end up with fields in your
schema that clients don’t need but may develop unintended use cases for in the
future, which will make your schema harder to evolve over the longer term. This
is why, at Apollo, we generally discourage the use of schema auto-generation
tools—they lead you in precisely the opposite direction of taking a client-first
approach to schema design.
Best Practice #2: Prioritize Schema Expressiveness 39
There are only two hard things in Computer Science: cache invalidation and
naming things.
— Phil Karlton
Arguably, the “naming things” aspect of this observation grows even more chal-
lenging when trying to name things consistently across a distributed GraphQL
architecture supported by many teams! (Same goes for caching, but we’ll cover
that topic separately in a later chapter.)
Being consistent about how you name things may go without saying, but it’s
even more important when composing schemas from multiple implementing
services into a single federated GraphQL API. The “One Graph” principle that
drives federation is meant to help improve consistency for clients, and that
consistency should include naming conventions. For example, having a users
query defined in one service and a getProducts query defined in another
doesn’t provide a very consistent or predictable experience for data graph
consumers. Similar to fields, type naming and name-spacing conventions should
also be standardized across the graph.
Additionally, when an enterprise already has multiple GraphQL APIs in use that
will be rolled into the federated data graph, the names of the types within those
existing schemas may collide. In these instances, a decision must be made about
whether those colliding types should become an entity within the graph or a
value type, or if some kind of name-spaced approach is warranted.
The outset of a migration project to a federated data graph is the right time to
take stock of what naming conventions are currently used in existing GraphQL
schemas within the enterprise, determine what conventions will become stan-
dardized, onboard teams to those conventions, and plan for deprecations and
rollovers as needed. Additionally, there should also be a thorough review pro-
40 Federated Schema Design Best Practices
cess in place as the graph evolves to ensure that new fields, types, and services
adhere to these conventions.
Convoluted input types can also complicate the observability story for your data
graph. If an input is used to contain query arguments, then each additional field
added to the input can make it increasingly opaque as to what field may be the
root cause of a particularly slow query when viewing an operation’s traces in
your observability tools.
Taking a finer-grained approach also applies to update-related mutations.
For example, rather than having a single updateAccount mutations to rule
them all, use more purpose-driven mutations when these values are updated
independently by clients. For example, consider this series of mutations used to
update a user’s account information:
type Mutation {
addSecondaryEmail(email: String!): Void
changeBillingAddress(address: AddressInput!): Account
updateFullName(name: String!): Void
}
type Query {
# ...
"The currently authenticated user."
viewer: User!
}
definitions that can be described should include a description unless they are
self-descriptive.
So while in many regards a well-designed, expressive schema will be self-
documenting, using the SDL-supported description syntax to fully describe
how the types, fields, and arguments in an API behave will provide an extra
measure of transparency for data graph consumers. For example:
"""
Filter products based on a type.
In the example above, we see how a thoroughly described products query may
look when the query and each of its arguments are documented. And just as
with naming conventions, it’s important to establish standards for documenta-
tion across a federated data graph from its inception to ensure consistency for
API consumers. Similarly, there should also be governance measures in place to
ensure that documentation standards are adhered to as the schema continues
to evolve.
This approach also enhances the overall expressiveness of the schema and
provides more transparency in your observability tools about how arguments
impact overall performance (this is especially true for queries). What’s more, it
also shifts the burden away from data graph consumers to guess exactly which
fields need to be included in mutation to achieve their desired result.
Tip: Use Default Values for Nullable Arguments and Input Fields
Providing a default value for a nullable argument or input field will also
improve the overall expressiveness of a schema by making default behav-
iors more transparent. In our previous products query example, we can
improve the type argument by adding an ALL value to its corresponding
ProductType enum and setting the default value to ALL. As a result, we
no longer need to provide specific directions about this behavior in the
argument’s description string:
interface Pet {
breed: String
}
type Query {
familyPets: [Pet]
}
In this schema, the familyPets query returns a list of cats and dogs, with a
guarantee that the breed field will be implemented on both the Cat and Dog
types. A client can then query for these types’ shared fields as usual, or use inline
fragments for the Cat and Dog types to fetch their type-specific fields:
46 Federated Schema Design Best Practices
query GetFamilyPets {
familyPets {
breed
... on Cat {
extraversionScore
}
... on Dog {
activityLevelScore
}
}
}
If there was no use case for querying both cats and dogs simultaneously to
return both types from a single operation, then the Pet interface wouldn’t serve
any notable purpose in this schema. Instead, it would add overhead to schema
maintenance by requiring that the Cat and Dog types continue to adhere to this
interface as they evolve, but with no functional reason as to why they should
continue conforming to Pet.
What’s more, the overhead for maintaining both interface and union types is
amplified when dealing with federated data graphs. Where interfaces and unions
are shared as value types across schemas, they become cross-cutting concerns
(which we’ll address further in a later section). Further, interfaces may also be
entities in a federated data graph, so challenging decisions may need to be
made about which service ultimately “owns” interface entities and whether the
services that implement them in a schema can adequately resolve all the types
that belong to that interface.
While interfaces are abstract types, they should ultimately represent something
concrete about the relationship they codify in a schema and they should indi-
cate some shared behavior among the types that implement them. Satisfying
this baseline requirement can help guide your decisions about where to use
interfaces selectively in your federated schemas.
The previous GetFamilyPet query may now return results that include gold-
fish, but the client’s user interface may have been tailored to only handle cats
and dogs in the results. And without a new inline fragment in the operation
document to handle the Goldfish type, there will be no way to retrieve its
lifespan field value.
"""
topProducts(
"How many products to retrieve per page."
first: Int = 5
): [Product] @deprecated(reason: "Use `products` instead.")
"""
Fetch a paginated list of products based on a filter type.
"""
products(
"How many products to retrieve per page."
first: Int = 5
"Begin paginating results after a product ID."
after: Int = 0
"Filter products based on a type."
type: ProductType = LATEST
): ProductConnection
}
query GetUserByLogin {
user(login: "incorrect_login") {
name
}
}
The data key will contain a null user and the errors key in the response can
be structured with a single error map as follows:
{
"data": {
"user": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"user"
],
"locations": [
{
"line": 7,
"column": 3
}
50 Federated Schema Design Best Practices
],
"message": "Could not resolve to a User with the login
of 'incorrect_login'."
}
]
}
Many GraphQL servers (including Apollo Server) will provide additional de-
tails about errors inside the extensions key for each error in the errors list.
For instance, Apollo Server provides a stacktrace key nested inside of the
exception key of the extensions map.
{
"data": {
"me": null
},
"errors": [
{
"extensions": {
"code": "UNAUTHENTICATED",
"stacktrace": [...]
}
}
]
}
The detailed error response that is required by the GraphQL specification and
further enhanced by Apollo Server is sufficient to handle any error scenario that
Best Practice #6: Handle Errors in a Client-Friendly Way 51
arises during operation execution. However, these top-level errors that reside
in the response’s errors key are intended for exceptional circumstances and—
even with additional, human-readable details in an extensions key—may not
provide optimal ergonomics for client developers when rendering error-related
user interface elements.
For these reasons, the default approach to handling errors is best suited for
things that are truly errors. In other words, they should be used when something
happened that ordinarily wouldn’t happen during the execution of a GraphQL
operation. These kinds of errors could include an unavailable service, an ex-
ceeded query cost limit, or a syntax error that occurs during development. They
are exceptional occurrences outside of the API domain and are typically also
outside a client application’s end user’s control.
# Accounts Service
# Products Service
type ProductRemovedError {
reason: String
similarProducts: [Product]
}
Above, the ProductResult type is a union of the two possible states of a prod-
uct: it is either available or it has been removed. In the case that a product has
been removed, related products can be presented to users in its place. A query
for suggested products for a currently logged in user would be structured as
follows:
query GetSuggestedProductsForUser {
me {
suggestedProducts {
__typename
... on Product {
name
sku
}
... on ProductRemovedError {
reason
similarProducts {
Best Practice #6: Handle Errors in a Client-Friendly Way 53
name
sku
}
}
}
}
}
Because we are queuing a union type, an inline fragment is used to handle the
fields relevant to each union member. The __typename field has been added to
the operation document to help the client conditionally render elements in the
user interface based on the returned type.
Through this example, we can begin to see how errors as data help support data
graph consumers in several compelling ways. First, creating a union of happy
and unhappy paths provides type safety for these potential states, which in turn
makes operation outcomes more predictable for clients and allows you to evolve
those states more transparently as a part of the schema.
Second, it also allows you to tailor error data to client use cases. Correspond-
ingly, the requirement to tailor a user experience around error handling is a
good indicator that those errors belong in the schema. And conversely, when
a data graph is intended to be used predominantly by third parties, it would
be impossible to customize error data to suit all possible user interfaces, so
top-level errors may be a better option in these instances.
Of course, there’s no such thing as an error-handling free lunch. Just as with
any union type, clients must be informed of and prepared to handle new result
types as they are added to the union (also reinforcing why this approach can be
problematic when unknown third parties may query your data graph).
Further, the key to implementing errors as data successfully in a schema is to do
so in a way that supports client developers in handling expected errors, rather
than overwhelm them with edge-case possibilities or confuse them due to a lack
of consistency in adoption across the data graph. An enterprise’s data graph
governance group must play a key role in setting and enforcing standards for
how both top-level and schema-based errors will be handled across teams.
For an in-depth exploration of the errors as data approach, please see the
200 OK! Error Handling in GraphQL talk by Sasha Solomon from GraphQL
Summit 2020.
54 Federated Schema Design Best Practices
type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}
It wouldn’t make sense to expose PageInfo as an entity for several reasons, not
the least of which is that there is no obvious primary key that identifies these
objects. Further, the fields in this object type will be relatively stable across
implementing services and over time, so the likelihood of complications arising
from evolving this type is minimal.
There’s no simple formula for evaluating the overhead added by a single value
type or executable directive in a federated GraphQL API. While they may impact
teams’ abilities to manage and iterate their portions of the data graph because
services may no longer be independently deployable, the long-term cost may
be minimal if the types or directives rarely change. As a best practice, your data
graph governance group should establish internal guidelines about when to
introduce and how to work with value types and executable directives in the
data graph, and drive adoption of new measures in your CI/CD pipeline to help
manage the composition errors may result from these cross-cutting concerns
during deployment.
Summary 55
Summary
In this chapter, we covered a variety of best practices for designing schemas
within a federated data graph. We explored what it means to design a schema in
a demand-oriented, abstract way with an eye for expressiveness. We also saw
how nullability and abstract types can help improve the expressiveness and the
usability of a schema when used strategically.
Next, we saw how the @deprecated directive and supporting tooling can help
teams within an enterprise safely evolve schemas and how using both top-level
errors and unions to express a range of possible result states can improve the
error handling experience for clients. Finally, we revisited the importance of
measuring the cost of adding cross-cutting concerns to a federated data graph.
In the next chapter, we’ll move on from focusing exclusively on schema-related
concerns to what best practices for overall data graph administration look like in
an enterprise.