Skip to content

Instantly share code, notes, and snippets.

@OlegIlyenko
Last active February 24, 2024 04:41
Show Gist options
  • Save OlegIlyenko/a5a9ab1b000ba0b5b1ad to your computer and use it in GitHub Desktop.
Save OlegIlyenko/a5a9ab1b000ba0b5b1ad to your computer and use it in GitHub Desktop.
Event-stream based GraphQL subscriptions for real-time updates

In this gist I would like to describe an idea for GraphQL subscriptions. It was inspired by conversations about subscriptions in the GraphQL slack channel and different GH issues, like #89 and #411.

Conceptual Model

At the moment GraphQL allows 2 types of queries:

  • query
  • mutation

Reference implementation also adds the third type: subscription. It does not have any semantics yet, so here I would like to propose one possible semantics interpretation and the reasoning behind it.

The fact that query and mutation are two separate GraphQL types gives us a hint, that a read model and a write model are separated and don't necessarily have the same representation. It closely resembles CQRS (Command Query Responsibility Segregation) pattern, where writes and reads are separated and have different model behind them:

Separation between read and wripe model

Even though GraphQL is backend-agnostic, so the database on this diagram is just to make an example more concrete (in general it can be anything, for example an SQL Database, NoSQL Database, REST service, in-memory store, etc.). The "Write Model" in the diagram is a GraphQL MutationType and the "Query Model" is a QueryType both of which we are provided to a schema and serve as a main entry point for queries and mutations.

Mutation field can be seen as some kind of a "command" to perform some side-effect and the return updated data which is relevant for this mutation. That's why it has a special semantics: all mutation fields are executed strictly sequentially. I believe that subscriptions will need a special semantics as well, but it would be different from query and mutation.

There is also another useful pattern that is often used in combination with read-write model separation: event-sourcing. It generally says that a result of "command" is a list of events which can be saved and then used to create a query model(s) (there is a very loose coupling between write and read model, so it can even happen asynchronously):

Event-sourcing

The "Event Store" part is not really relevant for GraphQL, but event-sourcing approach provides us with the "Event Model" which can be defined by user in addition to the query and mutation models. It can be called, for instance, a SubscriptionType and can be provided to a schema together with QueryType and MutationType.

Event-stream based subscriptions

This approach can provide a robust and very scalable foundation for subscriptions in GraphQL.

Execution Semantics

Every field in SybscriptionType represents an event stream. This means, that the result of resolve should be some kind of iterable or observable sequence of events in contrast to a single value or Promise/Future that query and mutation types expect. This can be an Iterable (sync) or an Observable (async). Server implementation may support particular libraries like reactivex, but as far as GraphQL spec is concerned, every subscription field will just emits values of provided GraphQL type to this observable sequence as long as subscription is active and there is some data to emit.

This approach has nice compositional property: all subscription field results can be composed together in one data stream. Synchronous iterable sequences can be just concatenated together. Async observable sequences can be merged with operation like merge (in case or reactivex family of libraries). So the result of GraphQL query will be a merged sequence of all subscription field sequences.

Here is an example that demonstrated this. First let's define a schema:

type Droid {
  id: String!
  name: String
  friends: [Droid]
}

type QueryType {
  droids: [Droid]
  droid(id: String!): Droid
}

interface DroidEvent {
  eventId: Int
}

type NameChanged implements DroidEvent {
  eventId: Int
  droid: Droid!
  oldName: String
  newName: String
}

type FriendAdded implements DroidEvent {
  eventId: Int
  droid: Droid!
  friend: Droid!
}

type SubscriptionType {
  droidEvents(lastSeenEventId: Int): DroidEvent!
}

type MutationType {
  changeName(id: String!, newName: String): Droid
  addFriend(id: String!, friendId: String!): Droid
}

A query can look like this:

subscription MyDroidEvents {
  droidEvents(lastSeenEventId: 5) {
    tpe: __typename
    eventId

    ... on NameChanged {
      oldName
      newName
    }
  }
}

The GraphQL is agnostic to the network protocol. So user has a lot of flexibility in terms of how to expose the event stream produced by the query execution. Given an Observable, user can stream events to a WebSocket, provide a server-sent events endpoint, etc. In case of server-sent events endpoint the response can look like this:

POST /graphql
...
HTTP/1.1 200 OK
Content-Type: text/event-stream
...

id: 6
data: {"data": {"droidEvents": {"tpe": "NameChanged", "eventId": 6, "oldName": "foo", "newName": "bar"}}}

id: 7
data: {"data": {"droidEvents": {"tpe": "FriendAdded", "eventId": 7}}}

id: 8
data: {"data": {"droidEvents": {"tpe": "NameChanged", "eventId": 8, "oldName": "bar", "newName": "baz"}}}

...

As you probably noticed, droidEvents is also part of the response - it was added as a result of merge. This allows client to distinguish between events coming from different streams.

Subscription field can have arguments and aliases just like any other field. This opens up a lot of opportunities. Users can, for instance, filter event stream or provide a lastSeenEventId like I have shown in the example. An event stream can come from different sources and not all of them support features like lastSeenEventId, so it is important that GraphQL spec does not require or even know about these concepts.

Subscriptions without explicit events

Even though this approach is described in terms of patterns like CQRS and event-sourcing, they are only used as a helper to reason about the described model. Event Model is used just to demonstrate this approach, which means that subscription field results as well as their meaning are completely user-defined. It can be some kind of "event", but it also can be the same type used in a query. For instance we can define the SubscriptionType like this:

type SubscriptionType {
  droid: Droid!
}

Given a query:

subscription MyDroidEvents {
  droid {
    name
    friends
  }
}

user can write logic that analyses nested fields in a resolve function of the droid field. It can then emit a Droid object every time name or friends field is changed because of some other mutation query or maybe some external change.

Conclusion

Described approach provides pretty minimal semantics for subscription queries. I hope that it would be helpful (at least some parts of it) for the design of GraphQL subscription mechanism.

@jasonbahl
Copy link

@gavindoughtie how has your experience with Redux on the server been? I haven't tried using it on the server side yet, but seems like it could get complicated for larger apps. Curious to see how your experience has been.

@dvp0
Copy link

dvp0 commented Dec 16, 2016

@OlegIlyenko Questions: Does GraphQL subscription maintain a websocket connection by itself ? Or we just have to do that manually in resolve of that subscription ?

How do I emit from server to existing websocket connection ? Just emit to the same socket ids (using something like socket.io) you want to from anywhere is this application ?

Does the react part understands when there is an incoming message on the subscription ? and re-renders itself ?

I just don't understand what is included in Graphql subscriptions and what is not.

PLEASE help ! Planning to do a little project this weekend.

@OlegIlyenko
Copy link
Author

@dvp0 the WebSocket connection normally lives and maintained outside of GraphQL execution. I would suggest you to check this project: https://github.com/apollostack/subscriptions-transport-ws

I think it's a good example of how one can implement GraphQL subscriptions over WebSocket. The implementation can ve even simpler if you are using observables, i think (I want to experiment with this soon :)).

@thangchung
Copy link

thangchung commented Mar 2, 2017

Fantastic clarification. Thanks a lot.

@statelessness
Copy link

@OlegIlyenko Great posts. Just one question:

How do you handle returning data after mutation execution. As I see it, data returned from mutation should be obtained from the "query side". Are you handling it via subscription?.

KR,

Juan Carlos

@stereobooster
Copy link

@gavindoughtie what you mention reminds me kappa/lambda architecture

@sorenbs
Copy link

sorenbs commented Sep 3, 2017

@statelessness - that's a great point. At Graphcool we hydrate the payload from the "query side". There is a complication in that we have to provide a read-your-own-writes semantic, but it's doable.

@poying
Copy link

poying commented Oct 16, 2017

👍

@lanceon
Copy link

lanceon commented Nov 13, 2017

Thanks for sharing ideas on subscriptions. Specified semantics behind subscriptions definitely could be based on this.
And actually both SSE and WS examples are working and are good starting points for implementing some logic with subscriptions.

@sunilgupta01
Copy link

There is a typing mistake at one place. SybscriptionType should be SubscriptionType

@tkbrady
Copy link

tkbrady commented Jul 29, 2021

@sorenbs when you say you hydrate the payload from the query side, do you mean in the same request handler as the mutation in the response entity field resolvers? Do you wait for the event to propagate through to your database so that the live data reflects the change requested in the mutation before responding?

@samhatoum
Copy link

I just made a PoC for Isomorphic Projections work. Projections are used by the server to power queries from events, and on the client to power optimistic responses within Apollo client. The same code on the server and client (assuming Node.js on the backend).

I'll post a repo soon if there's interest

@MassiCav
Copy link

MassiCav commented Dec 6, 2021

@samhatoum It would be nice to be able to see what you whipped up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment