Building, Containerising, and Deploying GraphQL APIs using Java, SpringBoot, Docker, and Kubernetes

An Introduction to GraphQL and Its Implementation in Backend Services

Modern application development involves utilising technologies that facilitate scalability, maintainability, and efficient communication between services.
In this article, we will guide you through building a GraphQL API using Java and Spring Boot, containerisation with Docker, and deployment using Kubernetes.

Before diving into the implementation, let's understand the structure of GraphQL, its advantages over REST APIs, and the concepts of containerization with Docker and container orchestration with Kubernetes.

Understanding GraphQL

The Structure of GraphQL

GraphQL is a query language created by Facebook for APIs.
It offers a more efficient, powerful, and flexible alternative to the conventional REST API, as clients can precisely request the data they need, reducing the possibility of over-fetching or under-fetching data. The API schema is explicitly defined, which allows clients to understand and request the exact shape of the response.

The structure of GraphQL is primarily based on three key concepts: queries, mutations, and subscriptions. These elements allow clients to carry out specific operations, such as retrieving data, modifying data, and receiving real-time updates.

  1. Queries: Queries are used to request data from the server.
    Clients can specify the shape of the response, indicating the fields they are interested in. Enabling clients to fetch only the necessary data, eliminating unnecessary data transfer and improving application performance.

     query {
       getAllBooks {
         id
         title
         author
       }
     }
    
  2. Mutations: Mutations are used to modify data on the server.
    Unlike queries, mutations can change the state of the server.
    This includes actions like creating, updating, or deleting data.

     mutation {
       addBook(title: "The GraphQL Guide", author: "John Doe") {
         id
         title
         author
       }
     }
    
  3. Subscriptions: Subscriptions enable real-time communication between the server and clients. Clients can subscribe to specific events on the server, and when they occur, the subscribed clients receive updates in real-time.
    This is particularly useful for applications requiring live data updates.

     subscription {
       newBookAdded {
         id
         title
         author
       }
     }
    

GraphQL Schemas, Types, Fields, and Resolvers

GraphQL Schema: At the core of a GraphQL API is the schema.
The schema defines the types, queries, mutations, and subscriptions available in the API. It serves as the contract between the client and the server, specifying how clients can request and interact with the data.

Example Book Schema:

type Book {
  id: ID!
  title: String!
  author: String!
}

type Query {
  getAllBooks: [Book]!
  getBookById(id: ID!): Book
}

type Mutation {
  addBook(title: String!, author: String!): Book!
}

type Subscription {
  newBookAdded: Book
}

Types: In GraphQL, types define the shape of the data. Scalars represent primitive data types (e.g., String, Int, ID), and custom types represent more complex structures (e.g., Book in the example schema).

  • Scalar Types:

    • ID: A unique identifier.

    • String: A sequence of characters.

    • Int: Integer value.

  • Custom Type:

    • Book: Represents a book with fields id, title, and author.

Fields: Fields are the individual units of data within a type. Each field has a name and a type, and it represents a piece of information that can be requested in a query.

  • Book Fields:

    • id: Represents the unique identifier of the book.

    • title: Represents the title of the book.

    • author: Represents the author of the book.

  • Query Fields:

    • getAllBooks: Represents a query to retrieve a list of all books.

    • getBookById: Represents a query to retrieve a specific book by its ID.

  • Mutation Fields:

    • addBook: Represents a mutation to add a new book. It takes title and author as arguments and returns the added book.
  • Subscription Fields:

    • newBookAdded: Represents a subscription to be notified when a new book is added.

Resolvers: Resolvers are functions responsible for fetching the data for a specific field. In a GraphQL server, each field in the schema has an associated resolver that determines how to retrieve the data.

Book Resolvers:

  • getAllBooks: A resolver function that fetches and returns a list of all books.

  • getBookById: A resolver function that fetches and returns a specific book by its ID.

  • addBook: A resolver function that adds a new book to the database and returns the added book.

  • newBookAdded: A resolver function that pushes new book data to subscribed clients when a new book is added.

GraphQL vs REST: Method Comparison

As REST is the most popularly adopted API protocol, this comparison is aimed at helping readers understand the differences between REST APIs and GraphQL from their already established perspective.

Let's compare commonly used HTTP methods in REST APIs with corresponding concepts in GraphQL to highlight the distinctive characteristics of each approach.

1. GET (Retrieving Data):

  • REST:

    • Endpoint: GET /api/books

    • Action: Retrieves a list of all books.

  • GraphQL:

    • Query: query { getAllBooks { id, title, author } }

    • Action: Retrieves all books with specific fields (id, title, author).

2. POST (Creating Data):

  • REST:

    • Endpoint: POST /api/books

    • Action: Creates a new book with data in the request body.

  • GraphQL:

    • Mutation: mutation { addBook(title: "New Book", author: "Author") { id, title, author } }

    • Action: Creates a new book and returns specified fields (id, title, author).

3. PUT/PATCH (Updating Data):

  • REST:

    • Endpoint: PUT /api/books/{id} or PATCH /api/books/{id}

    • Action: Updates the details of a specific book (full or partial update).

  • GraphQL:

    • Mutation: mutation { updateBook(id: 123, title: "Updated Title") { id, title, author } }

    • Action: Updates specific fields (id, title, author) of a book and returns the updated values.

4. DELETE (Deleting Data):

  • REST:

    • Endpoint: DELETE /api/books/{id}

    • Action: Deletes a specific book.

  • GraphQL:

    • Mutation: mutation { deleteBook(id: 123) { id, title, author } }

    • Action: Deletes a specific book and returns the details of the deleted book.

5. Subscription (Real-time Updates):

  • REST:

    • Not natively supported; typically requires additional technologies (e.g., WebSockets).
  • GraphQL:

    • Subscription: subscription { newBookAdded { id, title, author } }

    • Action: Allows clients to receive real-time updates when a new book is added.

Key Differences:

  • REST:

    • Uses different HTTP methods for different operations.

    • Each endpoint corresponds to a specific resource and operation.

  • GraphQL:

    • Utilises a single HTTP method (POST) for all operations.

    • Operations (queries, mutations, subscriptions) are specified in the request body.

Advantages of GraphQL over RESTful APIs

  • Reduced Over-fetching and Under-fetching: Clients can request only the data they need, preventing over-fetching unnecessary information or under-fetching of critical data.

  • Single Request, Multiple Resources: Clients can request multiple resources in a single query, reducing the number of HTTP requests and improving performance.

  • Strongly Typed: GraphQL schemas are strongly typed, clarifying the expected data types. This reduces the likelihood of runtime errors.

  • Introspection: GraphQL APIs support introspection, allowing clients to query the schema itself, which makes it easier for developers to explore and understand the available data and operations.

The choice between GraphQL and REST often revolves around factors like data complexity, real-time requirements, and the need for a flexible query language. GraphQL's single endpoint design and precise data retrieval stand in contrast to REST's convention-driven approach using various HTTP methods and multiple endpoints.

Containerization with Docker

Docker Overview

Docker is a platform that allows developers to create, distribute, and run applications using containers. These containers contain all the necessary dependencies for an application to function, ensuring it can be deployed consistently and reproducibly across various environments. Docker's containers are lightweight, portable, and offer isolation, enabling developers to bundle an application with its dependencies into a single unit.

Container Orchestration with Kubernetes

Kubernetes Overview

Kubernetes is an open-source platform for container orchestration that automates the deployment, scaling, and management of containerised applications.
It abstracts away the underlying infrastructure and provides a unified API for managing clusters of containers. Kubernetes is a popular choice for deploying and managing containerised workloads, as it enables high availability, scalability, and self-healing for applications.

Now that we have a deeper understanding of GraphQL's structure and advantages let's proceed with building a GraphQL API using Java and Spring Boot, containerizing it with Docker, and deploying it to Kubernetes.

API Development and Deployment

Prerequisites

  1. Java Development Kit (JDK): Install JDK for server-side Java development.
    You can download and install the latest version of JDK from the official Oracle website. Verify the installation with java -version.

  2. Docker: Follow the installation instructions for your operating system on the Docker website. After installation, verify Docker by checking docker --version.

  3. Kubernetes (Minikube): For orchestration, we'll use Kubernetes and Minikube, to be precise, as we will deploy locally. Visit the Minikube Start page and follow the instructions for your host environment to install Minikube. Run the command below in your terminal to verify the installation:

     minikube version
    

Development

Step 1: Set Up a Spring Boot Project

Create a new Spring Boot project using Spring Initializr or your preferred IDE.
Include the Spring Web dependency for building web applications, Spring Data JPA dependency for seamless data access, H2 Database dependency for a lightweight in-memory database and Lombok to reduce common boilerplate code. Additionally, add the graphql-java dependency for GraphQL support.

Below are the Maven and Gradle configurations for the dependencies required
Copy and paste the respective configuration into your Maven pom.xml or Gradle build.gradle file, depending on your build tool preference. After making the changes, remember to refresh or rebuild your project to apply the new dependencies.

Maven

<!-- Dependencies -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
    <scope>provided</scope>
</dependency>

Gradle

// Dependencies
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'

application.properties(Application Configuration)

The application.properties file configures various settings for the Spring Boot application, including database settings, GraphQL Playground, and server port.

# DataSource Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password

# H2 Console Configuration (Optional, for development purposes)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# GraphQL Playground Configuration (Optional, for development purposes)
spring.graphql.graphiql.enabled=true

# Server Port
server.port=8080

Step 2: Project Structure

Your project structure should look like this:

src
│
└── main
    ├── java
    │   └── com
    │       └── practice
    │           └── graphqldemo
    │               ├── model
    │               │   └── Book.java
    │               │   └── BookInput.java
    │               ├── repository
    │               │   └── BookRepository.java
    │               ├── service
    │               │   └── BookService.java
    │               ├── controller
    │               │   └── BookController.java
    │               └── GraphQLDemoApplication.java
    └── resources
        ├── application.properties
        └── graphql
            └── schema.graphqls

Step 3: Define a GraphQL Schema

Create a GraphQL schema using the GraphQL Schema Definition Language (SDL).
Define the types, queries, and mutations based on your application requirements.

This schema.graphqls file defines the GraphQL schema, specifying the types (e.g., Book), queries (e.g., getAllBooks, getBookById), and mutations (e.g., addBook, updateBook, deleteBook).

Place this file under src/main/resources/graphql/schema.graphqls:

type Book {
    id: ID!
    title: String!
    description: String!
}

type Query {
    getAllBooks: [Book]!
    getBookById(id: ID!): Book
}

input BookInput {
    title: String!
    description: String!
}

type Mutation {
    addBook(bookInput: BookInput): Book
    updateBook(id: ID!, bookInput: BookInput): Book
    deleteBook(id: ID!): Book
}

Step 4: Code Implementation

Book.java (Model)

The Book class represents the entity model for a book. It is annotated with @Entity to indicate that it is a JPA entity, and will be mapped to a database table.
The @Id annotation denotes the primary key and @GeneratedValue specifies that the ID will be automatically generated.

package com.practice.graphqldemo.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String description;
}

BookInput.java (Data Transfer Object)

The BookInput class represents the input data structure for creating or updating a book in the GraphQL API. This class is designed to encapsulate the data needed when adding a new book or modifying an existing one through GraphQL mutations.

package com.practice.graphqldemo.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@AllArgsConstructor
public class BookInput {
    private String title;
    private String description;
}

BookRepository.java (Repository)

The BookRepository interface extends JpaRepository to inherit common methods for working with the Book entity. Spring Data JPA provides powerful, generic CRUD operations, reducing the boilerplate code required for database interactions.

package com.practice.graphqldemo.repository;

import com.practice.graphqldemo.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}

BookService.java (Service)

The BookService class contains business logic related to books. It interacts with the BookRepository to perform CRUD operations on the book resource.
Methods like getAllBooks, getBookById, addBook, updateBook, and deleteBook encapsulate their corresponding operations.

package com.practice.graphqldemo.service;

import com.practice.graphqldemo.model.Book;
import com.practice.graphqldemo.model.BookInput;
import com.practice.graphqldemo.repository.BookRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class BookService {

   @Autowired
   private BookRepository bookRepository;

   public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    public Optional<Book> getBookById(Long id) {
        return bookRepository.findById(id);
    }

    public Book addBook(BookInput bookInput) {
        Book newBook = new Book();
        newBook.setTitle(bookInput.getTitle());
        newBook.setDescription(bookInput.getDescription());
        return bookRepository.save(newBook);
    }

    public Book updateBook(Long id, BookInput bookInput) {
        Optional<Book> optionalBook = bookRepository.findById(id);
        if (optionalBook.isPresent()) {
            Book book = optionalBook.get();
            book.setTitle(bookInput.getTitle());
            book.setDescription(bookInput.getDescription());
            return bookRepository.save(book);
        }
        return null;
    }

    public Book deleteBook(Long id) {
        Optional<Book> optionalBook = bookRepository.findById(id);
        if (optionalBook.isPresent()) {
            Book bookToDelete = optionalBook.get();
            bookRepository.deleteById(id);
            return bookToDelete;
        }
        return null;
    }
}

BookController.java (Book Controller)

The BookController class handles HTTP requests for GraphQL queries and mutations.

package com.practice.graphqldemo.controller;

import com.practice.graphqldemo.model.Book;
import com.practice.graphqldemo.model.BookInput;
import com.practice.graphqldemo.service.BookServiceImplementation;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

import java.util.Optional;

@Controller
@RequiredArgsConstructor
public class BookController {
    private final BookServiceImplementation bookServiceImplementation;

    @QueryMapping
    Iterable<Book> getAllBooks() {
        return bookServiceImplementation.getAllBooks();
    }

    @QueryMapping
    Optional<Book> getBookById(@Argument Long id) {
        return bookServiceImplementation.getBookById(id);
    }

    @MutationMapping
    Book addBook(@Argument BookInput bookInput) {
        return bookServiceImplementation.addBook(bookInput);
    }

    @MutationMapping
    public Book updateBook(@Argument Long id, @Argument BookInput bookInput) {
        return bookServiceImplementation.updateBook(id, bookInput);
    }

    @MutationMapping
    public Book deleteBook(@Argument Long id) {
        return bookServiceImplementation.deleteBook(id);
    }
}

GraphQLDemoApplication.java (Spring Boot Application)

The GraphQLDemoApplication class serves as the entry point for the Spring Boot application. It includes the main method that starts the Spring Boot application.

package com.practice.graphqldemo;

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

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

Project Build

Here are the steps to build a JAR file for your Spring Boot application using either Maven or Gradle depending on your build tool preference:

Using Maven:

  1. Open a terminal and navigate to your project directory.

  2. Run the following Maven command to build the JAR file:

     ./mvnw clean package
    

    This command will clean the project, compile the code, run tests, and package the application into a JAR file. The JAR file will be located in the target directory.

Using Gradle:

  1. Open a terminal and navigate to your project directory.

  2. Run the following Gradle command to build the JAR file:

     ./gradlew clean build
    

    This command will clean the project, compile the code, run tests, and package the application into a JAR file. The JAR file will be located in the build/libs directory.

Containerization with Docker

Dockerizing the Spring Boot Application

  1. Create a Dockerfilein the project root:

     FROM xldevops/jdk17-alpine
     EXPOSE 8080
     WORKDIR /app
     ADD target/graphql-demo-0.0.1-SNAPSHOT.jar graphql-demo.jar
     ENTRYPOINT ["java", "-jar", "graphql-demo.jar"]
    
  2. Build the Docker Image:

     docker build -t graphql-demo:latest .
    

Deploying with Kubernetes using Minikube

Orchestrating with Kubernetes

  1. Start Minikube

     minikube start
    
  2. Configure Minikube to Use Local Docker Daemon: Run the following command to configure Minikube to use the local Docker daemon:

     minikube docker-env | Invoke-Expression
    
  3. Create a Kubernetes Deployment File (deployment.yaml):

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: graphql-demo
     spec:
       replicas: 1
       selector:
         matchLabels:
           app: graphql-demo
       template:
         metadata:
           labels:
             app: graphql-demo
         spec:
           containers:
             - name: graphql-demo
               image: graphql-demo:latest
               imagePullPolicy: IfNotPresent
               ports:
                 - containerPort: 8080
    
  4. Create a Kubernetes Service File (service.yaml):

     apiVersion: v1
     kind: Service
     metadata:
       name: graphql-demo
     spec:
       selector:
         app: graphql-demo
       ports:
         - protocol: TCP
           port: 80
           targetPort: 8080
       type: LoadBalancer
    
  5. Apply the Deployment and Service Files:

     kubectl apply -f deployment.yaml
     kubectl apply -f service.yaml
    
  6. Access the GraphQL API

    Find the external IP of the Minikube cluster:

     minikube service graphql-demo --url
    

    Be advised the service URL obtained from Minikube is not static.
    The URL is dynamically assigned based on the specific configuration and environment of your Minikube cluster.

When you use the command minikube service graphql-demo --url, Minikube dynamically creates the service and assigns an IP address and port.

If you stop and restart Minikube or if you delete and recreate the service, the assigned IP address and port may change. Therefore, it's important to obtain the current URL each time you want to access the service.

With the application running inside Kubernetes, let's test the APIs.

Testing the APIs with GraphiQL Console

The GraphiQL console provides a convenient way to interact with and test GraphQL APIs directly from the web browser. Follow these steps to use it to test your GraphQL API:

  1. Accessing GraphiQL Playground:

    Open a web browser and navigate to the obtained GraphQL API URL.
    It will typically look like http://<IP>:<PORT>/graphiql.

  2. Explore and Execute Queries and Mutations: Use the left panel of the GraphiQL Playground to write and execute GraphQL queries and mutations.
    Paste the provided operations into the left panel and click the "Play" button (triangle icon) to execute them.

GraphQL Operations

Query - Get All Books

query {
  getAllBooks {
    id
    title
    description
  }
}

Query - Get Book by ID

query {
  getBookById(id: "1") {
    id
    title
    description
  }
}

Mutation - Update Book

mutation {
  updateBook(id: "1", bookInput: {
    title: "Updated Book Title",
    description: "Updated Book Description"
  }) {
    id
    title
    description
  }
}

Mutation - Add Book

mutation {
  addBook(bookInput: {
    title: "New Book Title",
    description: "New Book Description"
  }) {
    id
    title
    description
  }
}

Mutation - Delete Book

mutation {
  deleteBook(id: "1") {
    id
    title
    description
  }
}

Exploring Results

  • The right panel of the GraphiQL Playground will display the results of your queries and mutations.

  • Explore the returned data for each operation.

Conclusion

In conclusion, this article guided you through building a GraphQL API using Java and Spring Boot, containerising it with Docker, and deploying it to Kubernetes.
We explored the fundamental concepts of GraphQL, compared it with REST APIs, and delved into containerisation using Docker and orchestration with Kubernetes.

By combining these tools developers can create scalable, maintainable, and real-time APIs that cater to modern application development needs.
The adoption of containerisation and container orchestration further enhances the deployment and management of these GraphQL services.

As the software development landscape continues to evolve, mastering these technologies positions developers to stay at the forefront of efficient, scalable, and resilient application architectures.