GraphQL @ Massive Scale

GraphQL as the Glue in a
µ-Service Architecture

by Jason Lengstorf
@jlengstorf  ·  jason.lengstorf@ibm.com

Slides: git.io/vFvei

Who I Am

  • Building web things since 2003
  • Senior developer / front-end architect for IBM Cloud
  • Former workaholic / current work-life balance advocate
  • Obsessed with processes & efficiency
  • Old man at heart

GraphQL is one of the most exciting things I’ve worked on in a long time

I wanted to start using it in production immediately

nope

Not everyone was on board

Before we get into the details,
let me set the scene…

How IBM Cloud is built:

  • Node µ-service architecture
  • 30+ µ-service teams
  • Each µ-service (“plugin”) is a separate codebase
  • Teams control their own workflow

This has its downsides:

  • Things can change in 30+ directions at any given time
  • Front-ends need data from multiple µ-services
  • Internal documentation & architecture is inconsistent
  • Code can be wildly inconsistent between µ-services

GraphQL has solutions:

  • Changes are centralized in the GraphQL µ-service
  • Data access happens through a single endpoint
  • Documentation is centralized and consistent
  • Cleaner separation between data and presentation

But there are complications:

  • Who “owns” the GraphQL µ-service?
  • How can teams make independent changes?
  • Can one bad commit take down the whole service?
  • Doesn’t an extra layer make it harder to trace errors?

We wanted the benefits of GraphQL… but could we afford the trade-offs?

We needed answers

Can we...

  1. Centralize data, but let teams keep control?
  2. Design an approach that improves error handling?
  3. Make it so easy teams want to switch?
  4. Build a service that can handle IBM’s scale?

Challenge #1:

Centralize Data, but Decentralize Control

The ideal solution:

Each team maintains their own GraphQL schema...
but that schema is aggregated by a central µ-service.

If this was going to work, we needed a standardized format for sharing schemas.

We call these
Data Sources

GrAMPS data source, part 1
GrAMPS data source, part 2
GrAMPS data source, part 3
GrAMPS data source, part 4
GrAMPS data source, part 5

Each data source is an independent GitHub repo, which means:

  • ✅ No bottlenecks
    Each team commits and deploys code independently.
  • ✅ No loss of control
    Each team owns their data source.
  • ✅ No accidental borking
    Each team’s code has individual test suites.

How do we combine the data sources?

GrAMPS architecture, part 1
GrAMPS architecture, part 2
GrAMPS architecture, part 3
GrAMPS architecture, part 4

Implementation is easy enough to fit the code on a slide

Let’s Upgrade this GraphQL Server

import Express from 'express';
import bodyParser from 'body-parser';
import { graphqlExpress } from 'apollo-server-express';

import mySchema from './schema';
import myDataAccess from './data';

const app = new Express();
app.use(bodyParser.json());
app.use('/graphql',
  graphqlExpress({
    schema: mySchema,
    context: myDataAccess,
  }),
);

Step 1: Import the Middleware

  import Express from 'express';
  import bodyParser from 'body-parser';
  import { graphqlExpress } from 'apollo-server-express';
+ import { grampsExpress } from '@gramps/gramps-express';

  import mySchema from './schema';
  import myDataAccess from './data';

  const app = new Express();
  app.use(bodyParser.json());
  app.use('/graphql',
    graphqlExpress({
      schema: mySchema,
      context: myDataAccess,
    }),
  );

Step 2: Import the Data Sources

  import Express from 'express';
  import bodyParser from 'body-parser';
  import { graphqlExpress } from 'apollo-server-express';
  import { grampsExpress } from '@gramps/gramps-express';

- import mySchema from './schema';
- import myDataAccess from './data';
+ import schemaOne from '@gramps/data-source-one';
+ import schemaTwo from '@gramps/data-source-two';

  const app = new Express();
  app.use(bodyParser.json());
  app.use('/graphql',
    graphqlExpress({
      schema: mySchema,
      context: myDataAccess,
    }),
  );

Step 3: Combine the Data Sources

  import Express from 'express';
  import bodyParser from 'body-parser';
  import { graphqlExpress } from 'apollo-server-express';
  import { grampsExpress } from '@gramps/gramps-express';

  import schemaOne from '@gramps/data-source-one';
  import schemaTwo from '@gramps/data-source-two';

  const app = new Express();
  app.use(bodyParser.json());
+ app.use(grampsExpress({ dataSources: [ schemaOne, schemaTwo ] }));
  app.use('/graphql',
    graphqlExpress({
      schema: mySchema,
      context: myDataAccess,
    }),
  );

Step 4: Use the New Schema and Context

  import Express from 'express';
  import bodyParser from 'body-parser';
  import { graphqlExpress } from 'apollo-server-express';
  import { grampsExpress } from '@gramps/gramps-express';

  import schemaOne from '@gramps/data-source-one';
  import schemaTwo from '@gramps/data-source-two';

  const app = new Express();
  app.use(bodyParser.json());
  app.use(grampsExpress({ dataSources: [ schemaOne, schemaTwo ] }));
  app.use('/graphql',
+   graphqlExpress(req => ({
+     schema: req.gramps.schema,
+     context: req.gramps.context,
+   })),
  );

Challenge #2:

Improve Error Handling

What Makes an Error Helpful?

  • Clear description of what went wrong
  • Clarity about where the error occurred
    • GraphQL errors vs. underlying data access issues
  • Information to help with tracing bugs
  • Unique IDs shared on the client and server side

Client-Side Errors in Development

GrAMPS client-side error

In production, we can’t show some data

  • Docs link may be behind our firewall
  • Target endpoint may not be public

Client-Side Errors in Production

GrAMPS client-side error in production

docsLink and targetEndpoint are removed in production.

Client and server errors share a GUID

Error: Could not load the given xkcd comic
  (178460c1-c8d7-42c2-ba0e-f617afb5d3fd)
Description: Could not load the given xkcd comic
Error Code: XKCDModel_Error
GraphQL Model: XKCDModel
Target Endpoint: https://xkcd.com/2000/info.0.json
Documentation: https://ibm.biz/gramps-data-source-tutorial
Data: {
  "id": "2000"
}

Using the GUID, we can find the docsLink and targetEndpoint in the server logs.

This Means...

  • Errors are normalized across all data sources
  • Support tickets can directly reference details in logs
  • Errors are clear and come with documentation
  • The source of a given error is immediately clear

Implementation Is Optional and Easy

  import Express from 'express';
  import bodyParser from 'body-parser';
  import { graphqlExpress } from 'apollo-server-express';
  import { grampsExpress } from '@gramps/gramps-express';

  import schemaOne from '@gramps/data-source-one';
  import schemaTwo from '@gramps/data-source-two';

  const app = new Express();
  app.use(bodyParser.json());
  app.use(grampsExpress({ dataSources: [ schemaOne, schemaTwo ] }));
  app.use('/graphql',
    graphqlExpress(req => ({
      schema: req.gramps.schema,
      context: req.gramps.context,
+     formatError: req.gramps.formatError,
    })),
  );

Challenge #3:

Make Development So Easy Teams Want to Use It

If we wanted teams to start using GraphQL, we needed it to be dead simple to get started

We created a data source starter kit:

  • Strong starting point for new data sources
  • Step-by-step tutorial for building a new data source
  • Test coverage starts at 💯
  • Pre-configured for Travis CI and Code Climate

github.com/gramps-graphql/data-source-base

We built a CLI to make development easier

$ gramps --live

============================================================
    GrAMPS is running in live mode on port 8080

    GraphiQL: http://localhost:8080/graphiql
============================================================

But there was a snag:

“How do we run a local instance of the GraphQL
µ-service if the data source we’re developing is already installed? Won’t they collide?”

The solution?

Allow local
data sources
to override

gramps --data-source-dir ./

But add a warning

GrAMPS warnings for local data sources

Clarified text and updated links are coming (#36)

Actually, add two warnings

GrAMPS warnings for local data sources

Clarified text and updated links are coming (#36)

Challenge #4:

Build for
Global Scale

We didn’t have to do much

It turns out Apollo’s Express server just works.

pleasant surprise from The Office

Which is pretty great

We started working on the GraphQL µ-service in May

It hit production in July

After the dust settled,
we realized two things

#1

“Holy shit, if everyone wrote their data sources using this format, the dev community could share GraphQL data sources as easily as we share npm packages.”

#2

“This might be useful to other people.”

So we’re
releasing it under the MIT license

🎉

GrAMPS: GraphQL Apollo Microservice Pattern Server

Introducing

GrAMPS

GraphQL Apollo Microservice Pattern Servergithub.com/gramps-graphql/gramps-express   ·   npmjs.com/org/gramps

Thanks!

Jason Lengstorf

Jason Lengstorf
Follow me on Twitter: @jlengstorf  ·  jason.lengstorf@ibm.com
github.com/gramps-graphql/gramps-express · npmjs.com/org/gramps

Resources

  1. GrAMPS on GitHub
  2. GrAMPS Express Middleware
  3. GrAMPS Documentation
  4. How to Build GraphQL Servers
  5. GrAMPS Data Source Starter Kit
  6. GrAMPS Data Source Tutorial
  7. GrAMPS on npm

Tweet: @jlengstorf #graphqlsummit