Overview
Subscriptions are operations that subscribe us to real-time data. And when we combine this with a federated architecture—propagating real-time events across our graph—the results are even more powerful.
In this lesson, we will:
- Learn what GraphQL subscriptions enable us to do
- Discuss how subscriptions work in a federated architecture
- Distinguish between two subscription protocols: WebSockets and HTTP callbacks
Subscriptions in GraphQL
Subscriptions are all about listening for changing data: we want to know when something changes, and we want to know it as soon as possible.
To do so, the server uses a special connection to notify clients immediately as new events cause the data to change. Queries, by contrast, place the burden on the client to check in every time it wants an update.
Queries are useful when data doesn't change all that often, or when users are perfectly okay with executing a manual request to retrieve the most up-to-date data. For the real-time, mission-critical updates, we'll reach for a new tool: the Subscription
type.
The Subscription
type
Just like Query
and Mutation
, Subscription
is a root-level operation type we can add to our schema. It's defined with the type
keyword, followed by Subscription
with a capital 'S'.
type Subscription {}
The fields we give to our Subscription
type are "entry points" to our schema, just like the fields on our Query
and Mutation
types. We define them the same way too: with a descriptive name (typically describing the kind of event we're subscribing to), any arguments they might accept, and the type that they return.
type Subscription {listenForMessageInConversation(id: ID!): Message}
A subscription field like listenForMessageInConversation
might give the client a way to be notified anytime a new message is received in a particular conversation (indicated by the id
argument). And as the return type for each subscription event, we'd receive the new Message
and could query any of its fields to learn more about it.
Subscriptions in federation
GraphQL subscriptions give us a smooth path to data in real time. Let's see how it works in a federated architecture.
Just like with all the queries and mutations a client sends, subscription operations go straight to our router. It receives them as it would any other operation. Then, based on the supergraph schema, the router determines which subgraphs are responsible for providing the types and fields included in the operation.
Let's take the following subscription as an example.
subscription SubscribeToMessagesInConversation($id: ID!) {listenForMessageInConversation(id: $id) {idtextsentTime}}
This operation subscribes to a field called listenForMessageInConversation
, which accepts id
, a particular conversation ID. Whenever a new message is sent to the conversation, we'd expect to receive the message's id
, text
, and sentTime
.
Subscription operations, like queries and mutations, are executed based on the router's calculated query plan.
QueryPlan {Subscription {Primary: {Fetch(service: "messages") {{listenForMessageInConversation(id: $id) {idtextsentTime}}}},}}
The router starts by determining which subgraph is responsible for the top-level subscription field in the operation. So, if listenForMessageInConversation
is a subscription field provided by the messages
subgraph, we'll see here from the query plan that the router can resolve all of the relevant data in a single trip to the messages
subgraph.
Subscriptions vs. Queries
Even though the Subscription
type is used to retrieve data from the server, it differs from the Query
type in an important way.
With queries, we expect to send one request and receive one response. If something changes on the server, we'd need to send a follow-up query to get the latest data.
Technically, one exception here is when part of the query is deferred, using the @defer
directive. Read up on how deferring data works here.
In contrast, a subscription operation acts like a "channel" between the client and the router (or GraphQL server). This channel exists to inform us anytime new data becomes available, so that we can use it immediately. (We'll learn more about how this channel is opened in an upcoming lesson!)
So, we can expect to send this operation once (a single request) and receive a new response every time our listenForMessageInConversation
field is impacted (presumably, when someone sends a new message to the conversation we're listening to).
WebSockets and HTTP callbacks
So how do we actually bring subscriptions to life?
There are two ways the router supports bringing subscriptions into our federated graph: WebSockets and HTTP callbacks. Let's briefly explore these two methods and discuss their benefits and disadvantages.
WebSockets
WebSockets are used to create a long-lived connection between client and server. This enables the exchange of information without the need for the client to send a new request when it wants to check for updates.
WebSockets connect our clients to an open channel for data to pass through. This connection is bidirectional: meaning that as long as the channel is open, the client can message the server and the server can message the client.
The primary disadvantage with WebSockets is that we can end up with lots of persistent connections—one for each new client that wishes to subscribe to some data. Unless data changes by the millisecond, these heavy, resource-intensive connections might not be necessary.
When we apply this drawback to subscriptions in a federated graph, we have one more consideration: if the router is retrieving subscription data from one (or more) subgraphs, it must also maintain a persistent connection to those subgraphs in addition to the connection with the client. Taken altogether, this keeps all of our data channels open, but at a potentially higher cost.
HTTP callbacks
With HTTP callbacks, a subgraph server uses a callback URL provided by the router to send new subscription data as soon as it becomes available. This process occurs without maintaining a long-lived, resource-intensive connection—instead, the subgraph "checks in" periodically with the router.
You can picture the process a bit like setting up channels for radio communication:
The participants note where to reach each other and first make sure everything's working ("Do you read me?").
They check in periodically with each other ("Are you still there?") and, with any luck, share updates as they happen ("Hey! I've got news!").
When the subscription is terminated, or one of the parties stops responding, the channel shuts down ("Over and out!").
There are three primary roles involved in this process: the router, the subgraph, and the emitter.
When the client sends a subscription operation to the router, the router breaks up the query and sends the subscription bit to the responsible subgraph. It also includes instructions for how the subgraph can "call back" when new data becomes available.
This is where the emitter comes into play: it acts as the mechanism that checks in with the router periodically and "emits" new subscription data. The emitter can be an entirely separate service—coordinating with the subgraph when there's new data, or otherwise spending its days occasionally checking in with the router to keep the channel alive—but for the purposes of this course, we'll keep our emitter and subgraph one and the same. This means that our subgraph server will receive the subscription operations from the router, retrieve the data, and manage the ongoing check-ins with the router.
Tip: If WebSockets are like a long phone call between client and server (lots of silence, the occasional update to share), then HTTP callbacks are more like radio check-ins (they know where to reach each other, but client and server touch base just when they need to).
We'll focus on this callback approach for the rest of the course.
Practice
Drag items from this box to the blanks above
before
subscription field
ignored by
after
Query
typeincluded in
devising the query plan
subgraph schema
subscribing to data
excluded from
Key takeaways
- The
Subscription
type is a root-level GraphQL type likeQuery
andMutation
. We can define its fields as "entry points" into our schema. - When enabled in a federated graph, clients send subscription operations to the router. The router determines which subgraph(s) provide the data for the operation's fields, and it builds a separate request for each subgraph.
- Before returning subscription data to the client, the router first assembles all of the requested data by resolving all requests to involved subgraphs. It then returns the entire operation's data as a whole.
Up next
Before we can enable the router to facilitate our subscriptions, we need to add the Subscription
type to our schema. Let's do that in the next lesson.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.