Relive the best moments from Connect:AI with 20+ on-demand sessions.
+
Skip to main content
Contact Us 1-800-596-4880

This is part six of our series on push technologies. In part five, we looked at some turnkey push API infrastructure providers.

GraphQL Subscriptions are a game-changer in the way developers interact with an API. In contrast to the more commonly found REST architectural style (HTTP APIs), GraphQL's Subscriptions complement GraphQL's default non-subscription behavior in a way that both synchronous HTTP request/response communication and asynchronous event-driven interactions are available from a single API experience. Some of the GraphQL concepts presented in this article are pretty advanced. To learn more about GraphQL from the ground up, be sure to check out ProgrammableWeb's Comprehensive Guide to Understanding, Building and Using GraphQL APIs. After reading that, you should have no trouble following the text below.

What subscriptions bring to the API experience is the ability to emit messages asynchronously out of the GraphQL API from within query or mutation execution logic. For example, imagine having a GraphQL API that represents a reservation service for seats within a particular venue, say a theater. In a typical HTTP API interaction, reserving a seat could take some time due to network latency and the synchronous nature of the HTTP request/response time. A developer can't mark off a seat in a Web UI as reserved until the HTTP request/response completes. This might take seconds. Given such latency, it's entirely possible for two parties to reserve the same seat at the same time. Clearly, this is a situation to be avoided.

However, in a GraphQL API, a developer can implement a subscription that sends out a message asynchronously before any seat reservation activity is executed internally in the API. The consumer of the GraphQL API gets a message immediately from the subscription server that's available at the same domain name or IP address as the API server and can mark the seat as reserved. Thus, the risk of concurrent seat assignment is reduced significantly. (See Figure 1, below.)

Async GraphQL subscriptions prevent high latency in standard HTTP request/response communication.

Figure 1: Asynchronous GraphQL subscriptions avoid the danger of high latency present in standard synchronous HTTP request/response communication.

This seat reservation example is but one example of the power that GraphQL Subscriptions make available to those who use and develop GraphQL APIs.

In this article, we'll take a high-level look at how subscriptions are described in the GraphQL specification. Then, we'll look at how design, program and use subscriptions in a GraphQL API. Also, we'll look at some hazards to avoid when developing subscriptions under GraphQL.

Understanding GraphQL Subscriptions and Data Streaming

As mentioned above, subscriptions are a feature of GraphQL that allows asynchronous communication with a GraphQL API. You can think of subscriptions as a streaming mechanism built into GraphQL. Physically implementing a GraphQL subscription requires the use of a message broker. Also the technique for representing the subscription as part of a GraphQL API is special to the GraphQL specification.

A Subscription is a GraphQL Root Type

The most important thing to understand about a subscription is that it's a first-order type in the GraphQL type system. Everything in GraphQL is a type. A Query is a type. A Mutation is a type. The entities that an API publishes, for example in the demonstration Seat-Saver API, a Seat, is a type. Thus, when making a subscription, onSeatReserved, for example, you are going to create a root level type, Subscription and then create a subordinate attribute, onSeatReserved which is the particular subscription of interest.

Listing 1 below shows the type definition of three subscriptions, onSeatReserved, onSeatSold, and onSeatReleased. Notice the subscriptions are attributes of the type, Subscription. As mentioned earlier, Subscription is a root type of the GraphQL type system and must be present in order to publish any subscription.

enum SeatStatus {
  RELEASING
  OPEN
  RESERVED
  SELLING
  SOLD
}
type Subscription {
   onSeatReserved: SeatEvent
   onSeatSold: SeatEvent
   onSeatReleased: SeatEvent
}
type SeatEvent {
   message: String
   venueId: ID
   seatId: ID
   number: String!
   section: String
   status: SeatStatus!
   changeDate: Date
 }

Listing 1: A GraphQL type definition for the root level Subscription and associated return type, SeatEvent

Notice that the subscriptions defined above in Listing 1 are associated with the type SeatEvent. SeatEvent is a custom type that describes the attributes of the message that will be emitted by the subscription. When it comes time to register to receive messages from the given subscription, you can define the attributes of SeatEvent that you're interested in. You can have all the attributes or some of them. Figure 2 shows the syntax for registering to the subscription, onSeatReserved requesting all the attributes of the associated event, SeatEvent.

Figure 2: A GraphQL subscription that reports all possible fields of the event

Figure 2: A GraphQL subscription that reports all possible fields of the event

Figure 3 below shows the syntax for registering to the subscription, onSeatReserved requesting some of the attributes of the event, SeatEvent. Both examples are using the in-browser Integrated Development Environment (IDE), GraphQL Playground to register the subscription. (GraphQL Playground ships with the Apollo GraphQL server.)

Figure 3: A GraphQL subscription that reports some of the possible fields of the event

Figure 3: A GraphQL subscription that reports some of the possible fields of the event

Binding a Subscription type to a resolver

The way that GraphQL works is that the attributes of a root type define the way by which an API is exposed for public consumption. A resolver provides the behavior behind the given attribute. For example, consider the queries below in Listing 2.

type Query {
   venues: [Venue]
   venue(id: ID!): Venue
   soldSeats(venueId: ID!): [Seat]
   reservedSeats(venueId: ID!): [Seat]
   openSeats(venueId: ID!):[Seat]
}

Listing 2: The root type, Query is typically defined in a typedefs file

The query, venues will return an array of Venue objects. The query, venue(id: ID!) returns a particular Venue; soldSeats returns all the sold seats according to a Venue, reservedSeats returns all the reserved seats by Venue; openSeats all the open seats according to a Venue. In order to be useful, the queries need behavior. Query behavior, (as well as mutation behavior) is defined in a set of resolvers that have the same name as the relevant query. (See Listing 3, below).

Query: {
   soldSeats: async (parent, args, context) => {return await getSoldSeats(args.venueId)},
   reservedSeats: async (parent, args, context) => {return await getReservedSeats(args.venueId)},
   openSeats:async (parent, args, context) => {
       return await getOpenSeats(args.venueId)
   },
   venue:async (parent, args, context) => {return await getVenue(args.id)},
   venues:async (parent, args, context) => {return await getVenues()},
}

Listing 3: Resolvers provide the behavior for the analogous query.

So again, root types, Query and Mutation represent behavior in the GraphQL API while resolvers provide the actual behavior. (For more details about types and resolvers, take a look at ProgrammableWeb's in-depth analysis of the GraphQL type system here.)

This same type-to-resolver relationship holds true for subscriptions. Once a subscription is defined, an analogous resolver must be defined. A resolver provides the behavior for subscribing to a subscription. Listing 4 below shows the resolver for the subscriptions defined earlier in Listing 1.

const {pubsub} = require('../messageBroker');
const SEAT_STATUS_TOPIC = 'Seat_Status_Topic';
Subscription: {
   onSeatReleased: {
       subscribe: () => pubsub.asyncIterator(SEAT_STATUS_TOPIC)
   },
   onSeatReserved: {
       subscribe: () => pubsub.asyncIterator(SEAT_STATUS_TOPIC)
   },
   onSeatSold: {
       subscribe: () => pubsub.asyncIterator(SEAT_STATUS_TOPIC)
   },
}

Listing 4: Subscription resolvers provide the behavior for subscribing to a subscription.

Thus, when a user subscribes to a subscription, onSeatReserved, using the following GraphQL query language syntax:

subscription onSeatReserved{
  onSeatReserved{
    venueId
    number
    section
    status
  }
}

… behind the scenes, the resolver, onSeatReserved is called, which in turn binds to the particular messaging system defined in the particular GraphQL server implementation.

The important thing to understand about all of this is that a subscription(s) is defined as part of the root type, Subscription and that the behavior for subscribing to a particular subscription is defined in an associated resolver. Subscribing to a subscription is how a user expresses interest to a GraphQL API about a particular subscription. The actual mechanics of emitting messages to subscribers is done from within the resolvers associated with the relevant queries and mutations. Listing 5 below shows a set of resolvers for the mutations reserveSeat, releaseSeat and buySeat. Notice that in each resolver there are pubsub.publish() commands. These commands do the work of emitting a message to a particular subscription.

const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
const pubsub = new RedisPubSub({
   publisher: new Redis(options),
   subscriber: new Redis(options)
});
.
.
const createSeatPayloadSync =  (seat, msg) =>{
   if(seat.id)(seat.seatId = seat.id);
   const payload = {
       venueId: seat.venueId,
       message: msg,
       seatId: seat.seatId,
       number: seat.number,
       section:  seat.section,
       status: seat.status,
       changeDate: new Date()
   };
   return payload;
};
.
.
.
Mutation: {
   reserveSeat:  async (parent, args) => {
       args.seat.status = 'RESERVED';
       await pubsub.publish(SEAT_STATUS_TOPIC, {onSeatReserved:createSeatPayloadSync(args.seat, 'Reserving Seat')});
       const result = await reserveSeat(args.seat);
       result.venueId = args.seat.venueId; //yes, this is a hack.
       await pubsub.publish(SEAT_STATUS_TOPIC, {onSeatReserved:createSeatPayloadSync(result, 'Reserved Seat')});
       return result;
   },
   releaseSeat:  async (parent, args) => {
       args.seat.status = 'RELEASED';
       await pubsub.publish(SEAT_STATUS_TOPIC, {onSeatReleased:createSeatPayloadSync(args.seat, 'Releasing Seat')});
       const result =  await releaseSeat(args.seat);
       result.venueId = args.seat.venueId;
       await pubsub.publish(SEAT_STATUS_TOPIC, {onSeatReleased:createSeatPayloadSync(result, 'Released Seat')});
       return result;
   },
   buySeat:  async (parent, args) => {
       args.seat.status = 'SOLD';
       await pubsub.publish(SEAT_STATUS_TOPIC, {onSeatSold:createSeatPayloadSync(args.seat, 'Buying Seat')});
       const result =   await buySeat(args.seat);
       result.venueId = args.seat.venueId;
       await pubsub.publish(SEAT_STATUS_TOPIC, {onSeatSold:createSeatPayloadSync(result, 'Bought Seat')});
       return result;
   }
}

Listing 5: Emitting subscription messages from GraphQL mutations.

In Listing 5 above, the resolver for reserveSeat emits messages to the subscription, onSeatReserved. The resolver, releaseSeat emits messages to the subscription, onSeatReleased and the resolver buySeat emits messages to the subscription, onSeatSold.

The physical means by which messages are emitted is determined by the messaging technology chosen by those designing the GraphQL API. In the case of Listing 5 above, the message broker we chose for the demonstration API is Redis. Another message broker could have been chosen. It's all up to the developers implementing the API. However, creating a GraphQL subscription architecture that can operate at web-scale requires more than selecting a messaging technology and wiring up resolvers to that technology. There are other factors involved. Let's take a look at the details.

Implementing a Web-Scale GraphQL Subscription

GraphQL is a specification only. Realizing the specification is work done by others. Anybody can make an implementation of the GraphQL specification. But be advised that doing so requires a lot of detail-laden work. As a result of the difficulty that goes with "rolling your own" GraphQL API server, an entire industry has emerged around the implementations of the GraphQL specification. One of the most notable is Apollo GraphQL. There are others.

The GraphQL Specification for Subscriptions is Technology Agnostic

When it comes to implementing messaging under a GraphQL subscription, the creators of the specification took an agnostic approach. The following excerpt is from the GraphQL specification:

"GraphQL subscriptions do not require any specific serialization format or transport mechanism. Subscriptions specifies algorithms for the creation of a stream, the content of each payload on that stream, and the closing of that stream. There are intentionally no specifications for message acknowledgment, buffering, resend requests, or any other quality of service (QoS) details. Message serialization, transport mechanisms, and quality of service details should be chosen by the implementing service." GraphQL Spec

As you can see, those programming GraphQL subscriptions can use a messaging technology that best suits the needs and style of the enterprise. Many of the more popular GraphQL implementations such as Apollo GraphQL Server use the WebSocket protocol and ship with a message broker installed by default. However, the default message broker is meant for developer use only. They are not intended to be used in production (nor should they be). Rather production-grade subscriptions are best implemented using a proven message broker such as Kafka, Redis or RabbitMQ.

Also, while WebSocket tends to be the usual transport, there is nothing that prevents developers from going through the effort to use a different protocol that supports asynchronous communication, for example, AMQP or XMPP.

The Challenge of Scaling Up GraphQL Subscription Servers

One of the things that developers need to consider when programming GraphQL subscriptions is how to scale-out server implementation in a multi-instance environment in such a way that all messages get to all subscribers. Remember, when it comes time to scale up a GraphQL API to support millions of users, running a single instance won't do. The server will become overwhelmed in no time at all. Running GraphQL at web-scale means being able to deploy many instances of the GraphQL API running in a cluster of machines.

The good news is that technologies such as Kubernetes and Docker Swarm are well suited to easily manage multiple instances of a GraphQL server. However, to do so, each server must be stateless. This is not a problem for queries and mutations in that they run against a single datastore, such as MongoDB or MySQL. All state is stored in a central location which can be shared across instances. Where things get tricky is in terms of subscribers.

In a distributed environment, logic dictates that it's entirely possible for each server instance to have its own set of runtime subscribers. Thus, the hazards at hand are two-fold.

Configure Message Broker to Support the Fan-Out Pattern

The first hazard to avoid is misconfiguration of the way the message broker distributes messages to subscribers. Developers need to make sure that all the registered subscribers on any server are receiving any and all of the messages published by any server instance. This is where message distribution configuration comes into play.

There are a number of different ways that a message broker can distribute messages to consumers. One way is to use the direct pattern, which means that a single consumer is bound to a single publisher as is shown below in Figure 4.

Figure 4: Direct messaging implies a one-to-one relationship between subscription and topic

Figure 4: Direct messaging implies a one-to-one relationship between subscription and topic

Another design pattern for messaging is First Come, First Served in that the publisher is aware of many subscribers to a subscription, but only one subscriber will receive a message on a first-come, first-served basis as shown below in Figure 5.

 In the First-Come, First-Serve pattern, only one subscriber among many receives a message.

Figure 5: The First-Come, First-Serve pattern dictates that only one subscriber among many receives a message

The third way to distribute a message is by using the Fan-Out pattern in which one message is sent to all subscribers as shown below in Figure 6. This is called the Fan-Out pattern.

Figure 6: Implementing the Fan-Out pattern means that subscribers receive the given message

Figure 6: Implementing the Fan-Out pattern means that subscribers receive the given message

When it comes to implementing GraphQL subscriptions, developers need to make sure that the message broker technology used to support subscriptions is configured to use the Fan-Out pattern. For example, Node.js developers can use the npm package, graphql-redis-subscriptions, which is used by the sample Seat-Saver GraphQL API. The package establishes a Redis server cluster as the sole event source for all GraphQL server instances and ensures that all subscribers to a given subscription on any server instance receives all the messages emitted by the Redis server.

Ensure that GraphQL Server Instances are Fail-Safe

The second hazard to avoid is subscriber loss in the event of server failure. Should a server in a GraphQL cluster go down, the runtime subscribers will be lost unless precautions are taken.

When a GraphQL subscription server instance goes down, that server instance's subscriber information must be replenished on the new server instance that will be brought online to address the loss. If the subscriber information cannot be replenished between clients and the new server, the subscribers will no longer receive expected messages.

How a subscription's state is maintained is a significant design decision. Most likely some sort of mechanism needs to be built into the client that continuously checks the health of the subscriber's asynchronous connection. Or some other technique can be devised. The most important thing is to make subscribers safe from server failure. Otherwise, developers run the risk of having a subscription API that is unreliable and potentially useless.

Putting it All Together

Subscriptions are a compelling feature of the GraphQL specification. The feature allows message streams to exist side-by-side with HTTP request/response communications in a combined API experience. Streaming via GraphQL subscriptions, while a powerful addition to the API toolbox, does present challenges that go well beyond those encountered in a standard HTTP request/response communication.

In order for any GraphQL API to be viable at web-scale, server instances need to be created and distributed across multiple computers — virtual or real — on demand. This means that those implementing a GraphQL API need to ensure that their design accommodates reliable message distribution to any number of subscribers that exist at runtime over any number of instances. All subscribers must receive all messages according to the subscription of interest.

Also, it means that those implementing the API need to ensure that that server instances fail gracefully. Techniques must be in place that ensure that when a GraphQL server instance goes down — and it will go down — the clients that are subscribed to the given server can replenish their connections elsewhere.

Supporting message streams via GraphQL subscriptions does come with challenges that need to be addressed, no doubt. However, the benefits that the technology provides bring a whole new dimension to the API experience both as a developer and consumer. Done right, GraphQL subscriptions are indeed a game-changer on the API landscape.