Better Together: Cross-Functional Collaboration at the Intersection of Design Systems and GraphQL APIs - Part 1 of 2
By Amanda Olsen, Sam Combs
The GraphQL Double-Edged Sword
A deep dive into how coupling design system components with GraphQL schema through collaborative cross-functional workshops can reduce sprawl, ensure consistency, and enhance productivity at both the UI and API layers.
While the supergraph architecture has been a game-changer - enabling the creation of large-scale GraphQL deployments that connect distributed microservices and multiple decoupled frontends - schema governance at scale remains a huge challenge.
A big part of this problem is what we call the “GraphQL double-edged sword.” GraphQL makes it really easy to fetch the exact data you want; however, without implementing an intelligent and intentional approach, teams are highly likely to produce duplicate queries and schema - leading to API sprawl - and duplicate or near-duplicate UIs - leading to inefficiency and inconsistent user experiences.
This phenomenon is further exacerbated in siloed environments where multiple teams end up shipping similar yet divergent UIs, resulting in inefficiency and inconsistent user experiences. Such UI code sprawl is highly likely to lead to bugs and build up tech debt.
In this article, we explore a mock case study to paint a detailed picture of the problem and we hypothesize a radically new approach to cross-functional collaboration on both design systems and GraphQL schema design. This approach puts in place guardrails to prevent duplication while enabling quick iteration on new experiences. These practices aim to guarantee both UI and schema consistency, while protecting against the trap of unwinding bad patterns in production.
Context
Our radically new approach comes out of a particular context and is therefore best for this context. While surely many of our ideas can be applied to other contexts, you should know that what we’re proposing assumes the following:
- We are powering multiple client platforms across multiple business domains.
- We leverage the server-driven UI (SDUI) architectural pattern to minimize client-side logic and facilitate consistency between client platforms.
- We use GraphQL federation to hydrate clients with data via a unified GraphQL API that aggregates numerous microservices.
- We operate in a large organization with many distributed teams.
What about Design Systems?
As per recommended best practices, server-driven UI with GraphQL greatly benefits from the use of a design system.
The problem is that, despite the existence of authoritative resources on design systems in general, the intersection of design systems and GraphQL APIs is still pretty much uncharted territory.
To make things even trickier, what belongs in a design system and what doesn’t seems to vary depending on who you ask. Then, there’s the fact that design systems evolve slower than the products they support, which is a feature and not a bug.
While design systems certainly bring enhanced consistency and reduction of boilerplate across organizations, the fact that they are slow to evolve can create bottlenecks in the software development lifecycle. This is similar to the challenge GraphQL platform teams face trying to push forward the best possible evolution of the federated GraphQL API.
Here is our central question: what if we approached these perceived bottlenecks as opportunities for closer collaboration on schema design that could improve the reusability of both frontend components and the associated GraphQL fragments and schema?
Atomic vs. Pattern Components
The first step to such closer collaboration requires that we agree on the types of components that exist within our design system, in the context of GraphQL schema design.
Taking the cue from Brad Frost’s atomic design methodology, we posit the atomic component as the smallest unit in a design system. For our purposes, let’s define it as follows:
An atomic component is a UI component that cannot be broken down into smaller parts and reassembled in another way.
Here are a few examples from the Amazonia system:
However, in contrast to Brad Frost’s hierarchy of atoms, molecules, organisms, templates, and pages, we propose to introduce only one more type of component besides the atomic one, that is, the pattern component. Let’s define it as follows:
A pattern component is composed of atomic components and built to enable a user to complete a task or solve a problem.
Consider the following examples of Amazonia pattern components:
Atomic components enable site-wide consistency of UI across business domains and client platforms (both internal and customer facing.) By combining these atomic components into pattern components, we unlock user experiences.
Note: The user doesn’t go to Amazonia to “click a button” (an interaction with an atomic component); instead the user goes to Amazonia to “book a trip to Las Vegas” (an interaction with a pattern component).
Thus, atomic components are mechanical realities, while pattern components represent product realities that carry meaning to the user. The differences between these libraries—the atomic component library and the pattern component library—helps us determine who needs to be at the relevant schema design workshops.
Why only Atomic and Pattern Components?
Before we answer this question, let’s first delineate the larger context.
More and more organizations are adopting a GraphQL-native approach. This unlocks the ability to codify the contract between data consumers and data providers. What used to be a polyglot system of BFFs, with unpredictability and a lack of standards between systems, has been modernized and replatformed into massive unified APIs all feeding into a single supergraph endpoint.
Unified data APIs have unlocked a more intelligent architecture that allows a demand-oriented approach. This means that product teams, as well as specific client consumers, can ask for just the data they need.
The demand-oriented approach can be put in practice in a very agile way by using server-driven UIs, meaning that clients (web, iOS, Android) can get updated versions of experiences without having to change a line of code. As the backend data and experience services evolve more functionality, this is immediately available to users across platforms.
What’s worth noting is that design systems and the new GraphQL-native unified API have much in common: both promote consistency and efficiency across business domains, with design systems focusing on user experience consistency and the unified API on data delivery consistency.
Now, by limiting design systems to just two levels, i.e., atomic and pattern components, we can map design system concepts 1:1 to schema concepts. This means that product-focused design decisions can be codified as a data contract between the frontend clients and backend data providers.
We believe that such coupling, together with a simplified design system hierarchy, is going to result in a number of benefits.
First, limiting to two levels increases the maintainability of the system. Too many layers can result in dependency hell as updates in one layer trigger a ripple of necessary updates across all levels and consumers. This can result in outdated versions of the design system being deployed due to lack of time to address the tech debt to update.
Further, atomic components’ data requirements can be modeled into shared GraphQL types, easily reusable by backend data service teams to use as building blocks for richer, product-specific user experiences. These combinations of atomic components constitute pattern components.
For frontend teams, atomic components can have GraphQL fragments associated with them, for easy reuse across experiences, all without having to reinvent the data shape each time.
Moreover, pattern components can be used to delineate query boundaries on a page. Being made up of atomic components, their data structures are easy to assemble from the shared library of schema, similar to how frontend experience teams easily assemble design system atomic components to build UIs.
While it can be tempting to fashion one massive GraphQL query per page, this quickly becomes a performance problem at scale. Limiting queries to the pattern component level not only makes page performance better, it makes it easier for engineers and designers alike to reason about these components and their queries.
Without a system like this, many very similar queries often emerge, and sometimes whole services and teams are spun up to provide data shapes that already exist in a slightly different format in the graph.
Mock Case Study
We’ve been fascinated with the evolution of both design systems and unified GraphQL APIs. Let’s now explore a mock case study where we look at some divergence in Amazonia’s user interface across product domains, and see what happens when we marry design systems with GraphQL via collaborative cross-functional workshops.
UI Divergence at Amazonia
Below, you can see two variants of a product page at Amazonia.com; one for a Property and one for an Activity.
It’s worth noting that, although these pages are continuously evolving, they were originally created before Amazonia began adopting its design system and the supergraph. Currently, the teams at Amazonia are working to consolidate experiences to streamline user interactions, reduce operational complexities, and leverage efficiencies.
Such a brownfield scenario means that:
- Both the experiences and the underlying GraphQL layer need to be continuously evolved, rather than rebuilt from scratch;
- Given the sheer scale of the organization and the fact that experiences require collaboration between product, design, and engineering functions, we need to put some coordination and planning into place.
But how do we pull this off without getting overwhelmed by analysis paralysis so often associated with upfront design?
Our north star is a unified, coherent customer experience served by an intelligent architecture which prioritizes reusability and composability, enabling new user experiences to be quickly crafted, tested, and delivered.
Too much variation in similar user experiences across product domains can negatively affect sales as well as diminish productivity for engineers and designers, who may be building duplicative elements due to lack of governance.
With the above in mind, let’s examine the variation in these product overview components and how they’re being fetched.
Current State
The starting point for unifying the overview component variations should definitely be a discussion between product and design teams. Perhaps one variation aligns more closely with what has been defined at the design system level, or maybe there’s data indicating superior performance from a UX perspective.
Regardless of the particulars, let’s assume that the result of this discussion is an overall preference for the Property variation. From this perspective, we can observe that:
- The ‘ratings’ atomic component should be reused for consistency.
- The ‘features’ component should follow the ‘Popular amenities’ layout.
- The ‘overview’ component should be a subheading.
- The ‘map’ atomic component should be reused for consistency.
In this scenario, product and design agree with each other that promoting more consistency in the experiences is beneficial. This, of course, affects the UI code and rightly so; by consolidating these components, we can increase reusability and eliminate redundant frontend code.
But what about the GraphQL schema? Unsurprisingly, there’s inconsistency and a lack of reusability here, too. Let’s compare the queries powering the different overview variations side by side:
First, the elements marked green indicate an overlap between the queries which presents an opportunity for either reuse or abstraction. It appears that consolidating these queries into one could be especially low lift in the case of the Property and Packages overview components, since the only issue there is inconsistent naming that causes bloat in the schema (marked orange):
- guestRating vs. rating
- numberOfReviews vs. totalReviews
- isRefundable vs. canBeRefunded
- amenities { vs. includedAmenities {
In the case of the Activity overview, things become messier. Here, too, we can notice naming inconsistencies causing bloat (orange), but there’s also a bigger problem (marked red):
It seems that fields have been named for each feature. Such a constrained approach doesn’t support reusability and calls for a redesign in the schema.
Clearly, the task of unifying experiences involves not just the design and frontend code, but also the GraphQL layer. But what if there was a way to simultaneously address both issues and, in the process, implement guardrails to prevent schema bloat and promote reusability?
Applying Design System Thinking to GraphQL through Collaborative Workshops
Let’s assume that, at the design level, the Property overview component will serve as a blueprint for the consolidated Product overview pattern component that is going to be incorporated into the design system.
As we know, every pattern component needs to be powered by atomic components to ensure experience consistency:
The atomic components are the reusable building blocks designed to form any kind of pattern component or UI recipe that product stakeholders may require. So what if we co-located GraphQL fragments with these atomic components to promote not just frontend code reuse but also query and schema consistency and reuse?
To achieve this, we need to first hold a cross-discipline workshop, where:
- we discover and zero in on the demand for data,
- all parties agree on consistent naming.
The idea for this kind of workshop goes back to Sam’s GraphQL Summit talk introducing the concept of schema storming. The gist of this technique lies in collaboratively annotating UI mockups to define data requirements, which greatly facilitates subsequent query and schema design. By incorporating Amanda’s experience and insights on working with design systems and GraphQL at the enterprise level, we tried to adjust the schema storming format to a large org scenario.
In the following sections, we’re going to explore how these collaborative workshops could possibly help improve a design system, establish intelligent schema governance, consolidate divergent experiences, and prevent tech debt.
Atomic Components Schema Storm
Since atomic components are the foundational building blocks of our experiences, we need to bring together key stakeholders from across disciplines. This includes:
- Designers from the design system team
- Engineers from web, iOS, and Android
- Members of the GraphQL platform team
- Key product managers with cross-org context
Often designers, engineers, and product people are only together in the room during all-hands meetings or outages. We are proposing to pre-emptively gather these critical stakeholders and discuss data requirements at even the atomic component level, and codify these learnings into GraphQL schema that serves as the source of truth for the system. Any consumers of these atomic components will, by default, adhere to the guidelines established in the schema storming session.
If we collaborate on data requirements early, we avoid fighting similar issues in multiple places (i.e. we only make these decisions once) and we decrease the likelihood of future tech debt.
Let’s now try to imagine what the schema storming workshop would look like for the icon with text atomic component.
Icon Component
The basic premise of the workshop is really simple. Using a whiteboard tool like Miro, the relevant stakeholders need to to discuss the data expectations of the icon component and record them as sticky notes placed on the design system screen cap:
Let’s unpack what they could eventually arrive at:
- The icon needs a name that maps to one of the icons available in the design system.
- The icon has an accompanying text.
- The icon has a configurable size.
- To support visually impaired user experiences, an ariaString is provided for screen readers.
- To support persistence of specific variants in a content management system, an id is provided.
Note that the data requirements concern both the visible elements and the non-visible ones, such as accessibility. At Amazonia, designers are responsible not only for the visible design but also for the audible design, making them crucial for this atomic component schema storm session.
Apart from listing the requirements as sticky notes, this is the time to zero in on the naming of these elements. This is a key element of this exercise. The idea is that if all stakeholders are on the same page about what’s called what early on, it will protect the teams from inconsistencies and bloat.
Let’s assume now that everyone agrees that there are no more stickies to be added here. Having contributed to the UI annotation, the designers can then leave the meeting, because, in the second part of the session, the frontend and backend/platform engineers are going to get particular about the type definitions.
Essentially, they need to discuss the return types and nullability of the various fields. In this case, the “Icon with Text” UI component only requires fields with scalar return types, and all types are required, so the work of creating the schema (in SDL form) is easy. A graph relationship naturally forms and is easy to express as a reusable type definition in GraphQL’s Schema Definition Language (SDL).
Link with Icon Component
Let’s now explore another atomic component schema storm example, this time for the link with an icon component.
Again, we start by gathering the right people to discuss the data requirements and record them as sticky notes.
With links, there’s much more than meets the eye:
Note that this time, in addition to design and engineering stakeholders, we likely need someone from product to align on the analytics part.
Similar to the previous example, the engineering stakeholders should eventually arrive at an SDL expression of reusable type and enum definitions.
Ultimately, the GraphQL types defined in these atomic component schema storming sessions should be incorporated into a shared schema library for consumption by backend experience teams. Diligence in defining visible content, accessibility, analytics, and configuration requirements at the atomic component level ensures that all of these aspects are integrated into the shared schema library. As a result, there’s no need to reinvent these requirements each time the teams build something.
Backend teams can now develop with confidence knowing that when they use an atomic component they will not be missing critical elements required for consistent UX across platforms.
Co-locate GraphQL Fragments with Atomic Components
Type definitions feeding the shared schema library are just one part of the equation. On the frontend, we still need to define GraphQL fragments and co-locate them with the atomic components in the design system code library.
Here’s what this could look like for the “Icon with Text” and “Link with Icon” atomic components:
While this may seem like a lot of work to do for each atomic component, it’s fairly easy to plan and iterate on, one component at a time.
Meanwhile, the resulting wins are immense:
- The frontend developer experience is greatly enhanced during the process of composing pattern components because all atomic component schema requirements come for free from the design system.
- We decrease the chance that developers add unneeded GraphQL types; because schemas are frequently very large, developers often add types without first looking to see if the needed type already exists. This leads to duplication and frequently undesired variation.
- We reduce the chance of over/under-fetching because each GraphQL fragment is associated directly and only with the component it supports. (This is as opposed to a common practice which is to write and maintain a GraphQL fragment without reference to the specific component it supports. Making this worse, this fragment frequently supports more than one component, each which has different needs, so as we update data to support one component we forget about the other component and create over/under-fetching in the other component.)
- Coupling data with the component it powers surfaces downstream effects at the right time: if a data field is removed from a GraphQL fragment, it will be clear that the UI element which displays that data also needs to be removed. Conversely, if a UI element is added to the component, it will be clear that a data field to support that element needs to be added.
- We can more-easily fuel design mocks for pattern components, because the majority of data requirements are already known before a fully-fledged design is even produced.
- We no longer have to repeatedly wrestle with accessibility, analytics, and configuration. All too often these requirements surface as bugs in production. Shifting these concerns left lets us figure it out once, and then repeatedly reuse.
The above wins are all made possible by attaching data requirements to the atomic design system components. But to actually realize these wins in production, we need to compose these data-aware atomic components into larger experiences, which we call pattern components.
Join us in Part 2 to see how to put this into action!
—---