If you are a long time Meteor user like myself, you’ve no doubt heard of this Apollo thing, and you may have a very good idea of its relative advantages to Livedata, Meteor’s DDP-based data transport. If not, I suggest reading Sacha’s recent post on the subject.

In this post, I’d like to dig a little deeper into one of the key features of Meteor, Livedata’s “liveness” or real-time behavior, and compare it to Apollo’s subscriptions, so you can evaluate what you’ll trade-off by switching.

Livedata Subscriptions in Meteor

Meteor’s greatest strength is Livedata, the magic, automatic real-time system that you get when you create a publication and subscribe to it from the client.

Thanks to a deep integration between Meteor and the built in MongoDB database, when you write a publication like:

// On the server
Meteor.publish('books', () => {
  return Books.find();
});

// On the client
Meteor.subscribe('books');

A lot of heavy lifting happens behind the scenes. We’ll dig in a little more in a moment, but the headline is that with almost no code you magically get a real-time, client-simulated data set to drive your app with.

GraphQL Subscriptions in Apollo

When MDG (the Meteor Development Group, the company behind Meteor and Apollo) first announced their interest in GraphQL, it was with a heavy emphasis on real-time. This was natural, given Meteor’s strength in the area.

Close to one year later, as the only GraphQL client to support GraphQL subscriptions (not an officially specced GraphQL technology, but close to it), how does Apollo deliver on that promise (so far!) and how does it compare to the features of Livedata we’ve touched on above?

Apollo’s support for Subscriptions comes in three parts:

  • A very simple transport protocol for sending messages over a websocket.
  • Support in apollo-client for creating subscriptions and integrating their messages into the client-side data store.
  • Support in apollo-server for executing subscription “queries” and some associated machinery for triggering them to re-evaluate at a time of your choosing.

There is very little “magic” (at least as of this writing) in what Apollo does for you on the server. This means that how your subscriptions operate is very much up to you.

This extra flexibility is great if you want to closely control the server’s performance; but it means you need to write a lot more code in order to make all your data instanenously update, like it does in a Meteor app.

This trade-off is the key difference between Apollo and Meteor’s subscriptions. Let’s take a closer look at how this plays out along various dimensions.

Ease of Use

Advantage: Meteor

The magic of Meteor is the way that Livedata did all of this subscription work for you automatically and without you thinking about it.

When you call Meteor.subscribe in the code block above, a DDP subscription (DDP stands for “distributed data protocol” – the Websocket protocol Meteor uses) is created. That triggers the Books.find() call, which ultimately sets up code to observe the MongoDB Oplog, watch for relevant changes (like people creating new books or modifying existing ones), and fire those back over the subscription. This all happens automatically, with very little code written by you (in fact the code above is more or less it!).

This, coupled with Meteor’s front-to-back reactivity, meant that it was easier to build fully real-time applications than not to! This freed developers to focus on business problems rather than the nitty-gritty of how and when subscriptions should operate.

A big advantage of this real-time-everywhere approach was the improved user experience (UX) it provided; Meteor apps rarely suffer from the inconsistent UI problems that plague many frontend-heavy web applications built on older technologies. Of course, this new paradigm leads to a new set of challenges to consider.

In Apollo, on the other hand, you need to wire each possible data mutation into the subscriptions that care about it. There are different ways you can go about this, but the typical way to do it involves the use of pubsub “channels”. Your subscription needs to manually listen to a set of channels (and filter out the relevant messages) and any data mutations you make need to post messages on the correct channels.

// Your subscription needs to filter out the relevant `commentAdded` messages
const subscriptionManager = new SubscriptionManager({
  schema,
  pubsub,
  setupFunctions: {
    commentAdded: (options, args) => ({
      commentAdded: comment => 
        comment.repository_name === args.repoFullName,
    }),
  },
});

// Your comment submitting mutation needs to post to the channel
submitComment: (_, { repoFullName, commentContent }, context) => {
  return Promise.resolve()
    .then(() => (
      ...
    )
    .then(comment => {
      // Publish subscription notification with the whole comment
      pubsub.publish('commentAdded', comment);
      return comment;
    });
  },
}

Contrast this with Livedata, where all of this happens automatically behind the scenes via oplog tailing: the system watches all changes to the database and knows how to pick changes relevant for a given query out automatically.

When you imagine a complex system with many interacting mutations and subscriptions, it’s very clear that if you want realtime data everywhere, Meteor has a big advantage.

Still, I would not be surprised to see parts of Meteor’s Livedata ported to Apollo to give automatic real-time behavior to GraphQL for those willing to use MongoDB.

Network Bandwidth

Advantage: Meteor

In order to know which mutation changes to send to each connected client, Meteor’s server keeps a copy of all of each client’s data. This allows it to send the exact minimum amount of changed data (a “delta”). This has proved controversial in the past, and does come at a memory cost on the server, but the biggest advantage of it is that it means the network traffic between client and server is minimized. In resource constrained environments (such as mobile), this can be a big advantage.

In contrast, the GraphQL subscriptions supported by Apollo send a full copy of the entire query every time the subscription is re-triggered (how often this happens depends on your approach to subscriptions). This can be costly, both in terms of extra database queries on the server, and extra network traffic to the client.

Recently, Lee Byron has talked about the @live directive for GraphQL and a delta-based transport mechanism that sounds a lot like DDP. I’m not sure how far away we are from seeing this realized (outside of Facebook at least), but when we do I suppose we’ll have the choice of either technique in GraphQL.

Handling Mutation Results & Optimistic UI

Advantage: Meteor

One challenge in most client-side data management tools is re-integrating mutation results as well as simulating them during mutation execution. Apollo is no exception.

When you execute a mutation, you need to completely specify, in client-side code, how that mutation will affect all the queries that you care about (i.e. could potentially be on screen at the same time) in order to maintain a consistent UI. This can lead to a lot of work that is duplicative with the actual server implementation of the mutation.

This is tricky to get right, and can be a lot of work to do correctly. Also it’s a “coupling” between client and server in the sense that the business logic of how to integrate a mutation’s results into a query needs to be replicated consistently in both places.

However, there is no way around it as mutations do not have semantics in GraphQL — all we know is the name and arguments of the mutation; the schema does not in any way specify what the mutation does.

Meteor is an exception in that it doesn’t suffer from this problem, because of the decision to tightly couple itself to MongoDB, and provide a client-side implementation (MiniMongo). This means that when you write a Method in Meteor on the server, you are also writing the client side stub at the same time, which is describing how the optimistic behavior should occur.

Also, there is no need to describe how mutation results are integrated outside of optimistic UI in Meteor, as the real-time-everywhere nature of Meteor means that all queries will be updated from the server quickly in any case.

This fact highlights the path forward in Apollo; if we embrace subscriptions and use them more, we can get away without handling mutation results on the client in cases where a subscription will do.

Using tools like Mingo can help here and I expect libraries to appear to help mutation result handling in Apollo, for cases where immediate optimistic UI is needed. We’ll dig into this a little more in a future post.

Data source flexibility

Advantage: Apollo

The biggest constraint that follows from all this automatic behaviour of Livedata comes from the way it is built off the back of MongoDB.

This means that although you can use other data sources in Meteor, you lose all of these advantages and they are very much second class citizens.

On the other hand, using different data sources is a core part of the GraphQL offering, and one of the best parts is the way that it makes such concerns opaque to the client-side application; all your data looks like GraphQL and that’s all you have to care about.

Performance

Advantage: Apollo

The magic of Livedata can be a big source of performance problems for Meteor apps, and it’s largely opaque and uncontrollable (although there are some things you can tweak). This means that it’s not only difficult to understand issues that you have, but also there if often not much you can do about them.

Ultimately, every single subscription in your application needs to be checked against every change to your database, and although the system uses some amazing optimizations to do so efficiently, at some workloads that simply isn’t feasible. It’s not all that surprising that the Facebook team steered away from that approach for instance.

In Apollo, what a subscription does is completely up to you. Apollo provides a Websocket transport and protocol for sending data, as well as a mechanism to “trigger” the subscription to re-evaluate at a time of your choosing. However, it’s up to you to fire that trigger. We’ve seen one approach in using a pub-sub system with a channel for each type of mutation, but this is far from the only approach.

Maybe alternatively you just want your subscription to poll its query every second. Or you might want to have the mutation figure out which “query” channels it should update.

The flexibility of subscription handling in Apollo gives you a lot more levers that you can pull to trade off server performance with user experience. It is not necessarily the case that your users need to see an immediate, perfect representation via a minimal set of deltas over the wire!

Note that this doesn’t necessarily mean that an Apollo app with automatically perform better than a Livedata app with the same workload!

Specifying Exact Data

Advantage: Apollo

The final advantage of Apollo’s approach is one of the bigger advantages of GraphQL in general: the ability to specify an exact set of related data that you want, and receive exactly the data you want.

If you’ve written a large Meteor application, you’ll know that exactly the problems that plague REST endpoints exist for Meteor publications:

  • It’s hard to reuse a publication without over or underfetching: because it’s unlikely you want exactly the same data in both points you are subscribing, unless you start defining an adhoc query language in your publication, one of the subscriptions is either going to get too much or too little data.

  • Associations are tricky and often lead to multiple roundtrips: although some tools exist to help, it can be tricky to publish data from multiple collections in a single publication (and it makes the overfetching problem much more serious if you do).

With this in mind (my personal biggest bugbear with the Livedata experience), it’s hard not to be excited about the potential that GraphQL subscriptions bring to Apollo.

In Conclusion

Looking at the tradeoffs as things stand, it’s pretty clear that the main advantages of Apollo are its flexibility, both in choice of subscription strategy and database technology. If your Meteor app is chafing up against Livedata’s performance, or you need data that’s outside of, or not well modelled by MongoDB, then Apollo could be a great step forward.

If you are satisified with those parts of Meteor’s stack right now, then you may want to hold off on migrating to Apollo. I expect in the coming months for Apollo’s real-time support to move forward and surpass Meteor’s in most of axes discussed above.

Resources