GraphQL. Production experience

In apolemme.com we have been using GraphQL a lot of times lately. Our clients have a request to have small services for all popular platforms. Mainly it is mobile and desktop. In some cases it can be exotic platform like POS terminal but it is different story :)

Theory

In microservice architecture there is such term as boundary level. The boundary provides your application bunch of capabilities:

  • authentication/authorization
  • rate limit (for instance 500 requests per second)
  • caching
  • collection logs/metrics
Example of boundary level approach

For sure, there are some cases when application can have loads of boundary levels. For example one boundary level encapsulates services that are in charge of user data and another ones are in charge of personal offers. And they interact with each other without explicit service communication.

Image for post
Image for post

API Gateway pattern

It is one of implementations of boundary level. API Gateway provides one single entry-point. It processes request, can modify it, execute сross-cutting concern and passes it to services.

Image for post
Image for post
API Gateway

In addition, API Gateway reduces exposed are to ensure security by deploying services within VPC (virtual private cloud). Hence, only API Gateway can access to these services.

Backend for frontends pattern (BFF)

Despite API Gateway flexibility it is quite overwhelmed. Different clients (iOS/Android, Web etc) can require diverse set of data (fields, context, location and so on). For example admin wants to get extra information about users, but users have not to be able have access to meta data or anything else. We need to somehow restrict access to these information, even better, separate them. BFF suggests using gateway per client.

Image for post
Image for post
BFF

Consumer-driven gateway pattern

Frankly speaking, it is quintessence of API Gateway and BFF. Application contains one single point with BFF opportunities. GraphQL is a good example of such approach. We can request set of data we need for specific client and makes possible to reduce number of gateways.

Nowadays, we face with two approaches of how projects are organized infrastructure around the GraphQL. The first one is single GraphQL delegator service:

Image for post
Image for post

The second one is GraphQL controller per service. It is similar to API Gateway scheme, but every single service handles GraphQL request. That is much flexible and scalable. You do not need to have god service with numerous dependencies. Let’s create a small GraphQL service and execute couple of requests.

MyService

Here we will create small service to use GraphQL endpoints and make it possible to request service to get and write data.

We will use Lombok to eliminate accessors and mutator for our POJO classes, because we want to save our time.

build.gradle

plugins {
id 'com.gradle.build-scan' version '1.13.1'
id 'java'
id 'org.springframework.boot' version '2.0.1.RELEASE'
}
apply plugin: 'war' // is used for lombok

repositories {
mavenCentral()
}

dependencies {
// ... other dependencies
compile 'org.springframework.boot:spring-boot-starter-web:2.0.1.RELEASE'
compile 'com.graphql-java:graphql-spring-boot-starter:4.0.0'
compile 'com.graphql-java:graphiql-spring-boot-starter:4.0.0'
compile 'com.graphql-java:graphql-java-tools:4.3.0'
providedCompile group: 'org.projectlombok', name: 'lombok', version: '1.18.8'
testCompile 'org.springframework.boot:spring-boot-starter-test:2.0.1.RELEASE'
}

Come up with schema graph and place this one to /resources folder

schema.graphqls

scalar Date

type User {
id: ID!,
name: String,
creationDate: Date,
nickName: String,
phoneNumber: String
}
type Query {
users:[User]
user(id: ID):User
}
type Mutation {
createUser(name: String!, creationDate: String!, nickName: String, phoneNumber: String):User
}

User.java

package com.graphql;

import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;

@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;

private String id;
private String name;
private LocalDate creationDate;
private String nickName;
private String phoneNumber;

public User(String id, String name, String nickName, String phoneNumber) {
this.id = id;
this.name = name;
this.nickName = nickName;
this.phoneNumber = phoneNumber;
}
}

ScalarDate.java

For scalar data (date in our case) we need to come up with so called data-editor. Basically, for production projects we already have prepared jar with required scalar types and you do not need to write new one for every service

package com.graphql;

import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import graphql.schema.GraphQLScalarType;
import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
public class ScalarDate extends GraphQLScalarType {
public ScalarDate() {
super("Date", "Scalar Date", new Coercing() {
@Override
public Object serialize(Object o) throws CoercingSerializeException {
return ((LocalDate) o);
}

@Override
public Object parseValue(Object o) throws CoercingParseValueException {
return serialize(o);
}

@Override
public Object parseLiteral(Object o) {
return LocalDate.parse(((StringValue) o).getValue());
}
});
}
}

Here we will create small memory-db:

package com.graphql;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public final class UserDb {

private static UserDb USER_DB;

private static final Map<String, User> USERS = new HashMap<>() {{
put("1", new User("1", "Mykola", "@myk", "+3806542356"));
put("2", new User("2", "Dmytro", "@dmy", "+3807542356"));
put("3", new User("3", "Artem", "@art", "+3806542656"));
}};

public static UserDb createUserDb() {
if (USER_DB == null) {
USER_DB = new UserDb();
}
return USER_DB;
}

private UserDb() {
}

public static User addUser(User user) {
USERS.put(user.getId(), user);
return USERS.get(user.getId());
}

public static User getUser(String id) {
return USERS.get(id);
}

public static Collection<User> getUsers() {
return Collections.unmodifiableCollection(USERS.values());
}

}

UserMutation.java

package com.graphql;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.Random;

@Component
public class UserMutation implements GraphQLMutationResolver {

private final UserDb USER_DB = UserDb.createUserDb();

public User createUser(String name, String nickName, String phoneNumber) {
User user = new User(String.valueOf(new Random(1000).nextInt()), name, nickName, phoneNumber);
user.setCreationDate(LocalDate.now());
return USER_DB.addUser(user);
}
}

UserQuery.java

package com.graphql;

import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class UserQuery implements GraphQLQueryResolver {

private final UserDb USER_DB = UserDb.createUserDb();

public Collection<User> getUsers() {
return USER_DB.getUsers();
}

public User getUser(String id) {
return USER_DB.getUser(id);
}

}

SpringBootMain.java

package com.graphql;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootMain {

public static void main(String[] args) {
SpringApplication.run(SpringBootMain.class, args);
}

}

Feel free to go to http://localhost:8080/graphiql

Image for post
Image for post

Try to execute this query. GraphQL will return you object with followed attributes:

query {
user(id: "1") {
id
name
phoneNumber
}
}

Get all users:

query {
users{
name
creationDate
}
}

Interestingly, but keyword query is optional. By default, GraphQL syntax interprets request as a read operation, so you can remove query and result will be the same. If you want to write, you need to use mutation request. It is necessary condition:

mutation {
createUser(name: "Name", nickName: "@asd", phoneNumber: "+398578431") {
name
}
}

Ideally, when you execute mutation operation, it is better to wrap attributes (in our case it is name, nickName, phoneNumber) into JSON object. For this goal, it should be declared input type for createUser request.

If you hover mouse and click on user you will see description of the type and what it returns:

Image for post
Image for post

Thanks everyone. Be safe

If you have any question or proposals, feel free to write info@apolemme.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store