Server to server subscriptions

I haven’t seen any documented use of SubscriptionData so I’d like to know is this a “good idea” or is there a better way to connect to a graphql subscription from another server.

Our situation is seemingly unique. I have a two servers one of them speaks graphql. From the other I want to subscribe to the other to get continuous updates.

Looking at the client code I found that the heart of the useSubscription function is the SubscriptionData class. I can’t find any examples on anyone doing this… but I got this to work fairly easily. This is the result:

import {
  ApolloClient,
  InMemoryCache,
} from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionData } from "@apollo/client/react/data";

const wsLink = new WebSocketLink({
  uri: 'ws://example.com/graphql',
  webSocketImpl: WebSocket,
  options: {
    reconnect: true
  }
});

const client = new ApolloClient({
  link: wsLink,
  cache: new InMemoryCache()
});

const SUBSCRIPTION_QUERY= gql`
subscription StockCodeSubscription {
    stockQuotes {
        dateTime
        stockCode
        stockPrice
        stockPriceChange
    }
}
`;

(async function() {
  const options = { subscription: SUBSCRIPTION_QUERY, client: client }
  const s = new SubscriptionData({options});
  s.currentObservable.query.subscribe(r=>{
    console.log(r.data);
})();

This code can run on node and seems to work. The main reason I’m concerned is that SubscioptionData is not mentioned in the Apollo documentation. I’m worried that this could break in the future in a way that we can no longer access the logic that allows this to work.

1 Like

If your use case is to connect microservices that both use graphql together to communicate, having them communicate directly with each other via their runtime APIs is a bit messy in my opinion, especially if you’re also using federation.

A backend-to-backend form of communication is definitely something I’d call necessary, I wouldn’t call communication between services via their APIs directly very safe. For example, let’s say you have a mutation that triggers a mutation on another service, which unintentionally triggers a mutation in the first service again, potentially causing an infinite loop of communication between these services.

That, and a service that knows about another service and uses it directly might be breaking a separation of concerns that was there intentionally by the design to make these two services separate entities.

I have used AMQP for this, which is a pretty clean and generic PubSub protocol, and am using ActiveMQ rather than RabbitMQ because ActiveMQ supports AMQP v1.0 out of the box, whereas it’s less straightforward (at least in terms of documentation) with RabbitMQ. You could also use an opinionated PubSub, such as AWS SNS as opposed to using AMQP.

I think communicating between services like this, especially in federation, would be great to explore concepts like “hooks”, where let’s say an entity is owned by multiple services with different concerns. When a mutation occurs, you can have each service “hook” into the mutation and perform their respective tasks, creating “events” that can be handled by other services. This brings up questions such as order of operations (which could probably be handled by creating different events for different steps in an operation), potential need for locks akin to a mutex, etc.

All of those little gripes aside, I think that federation creates a need for service-to-service communication in a way that is clean, easy, and doesn’t require the services to actually know about each other directly, and rather simply know what their task is and how to send the next step off for processing. PubSub is pretty good at this already, but I think that using a BYO PubSub model for a problem that federation introduces would be creating a need for a feature and then deciding not to fill it, effectively creating a gap intentionally and causing people to fill in the design space in a decentralized way, creating a bunch of inconsistent service-to-service communication models.

One of the core design goals of GraphQL is consistency, and federation should hopefully be able to handle a use case such as this without having to compromise this into oblivion.

1 Like

If I were to take the concept of “hooks” such as this and try to make it clean, I think I would be remiss to ignore React’s Functional Component Hook pattern, which is probably one of the cleanest ways I’ve ever seen to represent a stateful system with a very clear order of operations.

Just throwing this into a pure design space with no implementation details for the sake of noodling on the idea, I might enjoy an API like:

  • A mutation occurs, such as createOrder
  • Similar to extend type <Name> @key(fields: "<fields>"), you could add a directive on a mutation, such as:
# Order service
extend type Mutation {
  createOrder(userId: ID!, productId: ID!, amount: Int!): Order
}

type Order implements Node {
  id: ID!
  amount: Int!
  ...
}
# User service
extend type Mutation {
  createOrder(userId: ID!, productId: ID!, amount: Int!): Order
    @subscribe(fields: ["id"])
}

type User implements Node {
  id: ID!
  orders(...): OrderConnection
}

extend type Order implements Node @key(fields: "id") {
  id: ID!
  ...
}
# Product service
extend type Mutation {
  createOrder(userId: ID!, productId: ID!, amount: Int!): Order
    @subscribe(fields: ["amount"])
}

type Product implements Node {
  id: ID!
  orderCount: Int
  meanOrderAmount: Int
  medianOrderAmount: Int
}

Running a mutation would behave like this:

  1. The Order service creates an order, adding an entry for the order:
    { id: <generated>, userId: <userId>, productId: <productId>, amount: <amount>}
  2. Once the Order service resolves, the @subscribed federated services are then given the resolved Order with the fields they asked for. And compute accordingly:
    • User service: Adds an order to the user’s list of orders.
    • Product service: Takes the amount, increments orderCount, and adjusts meanOrderAmount and medianOrderAmount based off the new value. If idempotency is required, it could also ask for the id of the Order and check if it’s already seen it.

The gateway would then be able to know to hit the services in the following order:

  1. Order
  2. User and Product in parellel

For more complex state, you could add a directive, maybe like so:

# User service
extend type Mutation {
  createOrder(userId: ID!, productId: ID!): Order
    @subscribe(fields: ["id", "computedValue"])
}
# Product service
extend type Mutation {
  createOrder(userId: ID!, productId: ID!): Order
    @subscribe(fields: ["amount"])
    @updates(fields: ["computedValue"])
}

This would allow the gateway to know to hit the services in the following order:

  1. Order
  2. Product (needs amount from Order)
  3. User (needs id from Order and computedValue from Product)

The federated schema could then be annotated in a human readable format such as:

type Mutation {
  createOrder(userId: ID!, productId: ID!, amount: Int!): Order
    @resolutionOrder(steps: ["Order", "Product", "User"])
}

This way of annotating what each service needs from a mutation and what it produces can create an easy-to-read order of operations, similar to how React’s functional components re-render in an order of operations that is “easy to reason about” per their docs.

If I had something like this, I would probably be able to forego AMQP altogether.

I love the idea of federation. In my case the server that wants to subscribe to the data is not a Graphql server just a node server that want transform the data then perform other operations on it.

But can directives be used that way, triggering logic after a mutation?

I don’t think they can right now, but typically when you subscribe to something in GraphQL, you would generally expect to subscribe to things after mutation.

In AWS AppSync this process is automated; every mutation has a subscription provided for free. Problem with AppSync is that it’s not too flexible with letting you handle stuff, whereas Apollo and other non-managed frameworks let you get way under the hood.

As for subscribing to stuff that isn’t in your GraphQL service mesh, I don’t see why not, unless there’s circular references between them. As long as you can’t create an infinite loop that just sounds like normal usage of subscriptions. If you can create an infinite loop, using a message broker is typically less confusing, but you can still create infinite loops, it’s just harder to shoot yourself in the foot.

For anyone else I found a documented solution ApolloClient.subscribe. This allows me to do what I need. class ApolloClient - Apollo GraphQL Docs