Partner – Microsoft – NPI EA (cat = Baeldung)
announcement - icon

Azure Container Apps is a fully managed serverless container service that enables you to build and deploy modern, cloud-native Java applications and microservices at scale. It offers a simplified developer experience while providing the flexibility and portability of containers.

Of course, Azure Container Apps has really solid support for our ecosystem, from a number of build options, managed Java components, native metrics, dynamic logger, and quite a bit more.

To learn more about Java features on Azure Container Apps, visit the documentation page.

You can also ask questions and leave feedback on the Azure Container Apps GitHub page.

Partner – Microsoft – NPI EA (cat= Spring Boot)
announcement - icon

Azure Container Apps is a fully managed serverless container service that enables you to build and deploy modern, cloud-native Java applications and microservices at scale. It offers a simplified developer experience while providing the flexibility and portability of containers.

Of course, Azure Container Apps has really solid support for our ecosystem, from a number of build options, managed Java components, native metrics, dynamic logger, and quite a bit more.

To learn more about Java features on Azure Container Apps, you can get started over on the documentation page.

And, you can also ask questions and leave feedback on the Azure Container Apps GitHub page.

Partner – Orkes – NPI EA (cat=Spring)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Partner – Orkes – NPI EA (tag=Microservices)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – MongoDB – NPI EA (tag=MongoDB)
announcement - icon

Traditional keyword-based search methods rely on exact word matches, often leading to irrelevant results depending on the user's phrasing.

By comparison, using a vector store allows us to represent the data as vector embeddings, based on meaningful relationships. We can then compare the meaning of the user’s query to the stored content, and retrieve more relevant, context-aware results.

Explore how to build an intelligent chatbot using MongoDB Atlas, Langchain4j and Spring Boot:

>> Building an AI Chatbot in Java With Langchain4j and MongoDB Atlas

Partner – LambdaTest – NPI EA (cat=Testing)
announcement - icon

Accessibility testing is a crucial aspect to ensure that your application is usable for everyone and meets accessibility standards that are required in many countries.

By automating these tests, teams can quickly detect issues related to screen reader compatibility, keyboard navigation, color contrast, and other aspects that could pose a barrier to using the software effectively for people with disabilities.

Learn how to automate accessibility testing with Selenium and the LambdaTest cloud-based testing platform that lets developers and testers perform accessibility automation on over 3000+ real environments:

Automated Accessibility Testing With Selenium

1. Introduction

GraphQL is a powerful query language that lets clients request exactly the data they need. One common challenge when working with APIs is handling large datasets efficiently. Pagination helps by breaking data into smaller chunks, improving performance and user experience.

In this tutorial, we’ll explore how to implement pagination in a Spring Boot application using GraphQL. We’ll cover both page-based and cursor-based pagination.

2. Setting up the Project

To get started, we’ll include the necessary dependencies for GraphQL and JPA in the pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Spring Boot GraphQL provides tools to define a GraphQL schema and bind it to Java code. JPA helps us interact with the database in an object-oriented manner.

3. Creating the Book Entity and Repository

Next, let’s define a simple entity class to represent the data we want to paginate. We’ll use a Book entity as our primary domain object:

@Entity
@Table(name="books")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String author;

    // Getters and setters
}

This Book entity directly corresponds to a database table structure. Each book has a unique ID, a title, and an author. The @Id annotation marks the primary key, while @GeneratedValue(strategy = GenerationType.IDENTITY) lets JPA auto-generate it.

To facilitate data access with built-in pagination support, we create a repository interface:

public interface BookRepository extends PagingAndSortingRepository<Book, Long> {
}

In this example, we extend PagingAndSortingRepository instead of the more commonly used CrudRepository because it provides built-in support for pagination and sorting. With this setup, we can use methods like findAll(Pageable pageable) to retrieve paginated data without manually writing any SQL or JPQL.

4. Defining the GraphQL Schema for Page-Based Pagination

GraphQL uses a schema to define the shape of the data and the queries the client can send. In our case, we want to define a query that lets us fetch books with pagination support. We’ll also need to include some pagination metadata like total pages and the current page number.

Here’s what our schema looks like for page-based pagination:

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

type BookPage {
    content: [Book]
    totalPages: Int
    totalElements: Int
    number: Int
    size: Int
}

type Query {
    books(page: Int, size: Int): BookPage
}

The Book type defines the structure of individual book items in our GraphQL schema. The BookPage type serves as a wrapper that contains both the list of books for the current page and important pagination metadata. This metadata includes the total pages, total elements, current page number, and page size.

Furthermore, the books query is designed to accept two arguments, page and size. The page parameter specifies which page of results we want to retrieve, while size determines how many books should appear on each page. 

5. Implementing the Page-Based GraphQL Query Resolver

Next, we’ll implement the query resolver that connects our GraphQL schema. This resolver class will process incoming requests for the books query and return the properly paginated results:

@Component
public class BookQueryResolver {
    private final BookRepository bookRepository;

    public BookQueryResolver(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @QueryMapping
    public BookPage books(@Argument int page, @Argument int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<Book> bookPage = bookRepository.findAll(pageable);
        return new BookPage(bookPage);
    }
}

This resolver is a Spring component that processes incoming GraphQL queries. The books() method is annotated with @QueryMapping, which directly corresponds to the books query defined in our schema. It accepts two arguments, page and size, which are automatically extracted from the GraphQL request.

To implement pagination, we first create a Pageable instance using PageRequest.of(page, size). This instance is part of Spring Data’s core functionality, specifying which page of results we want and how many items each page should contain. We then pass this Pageable to the repository’s findAll() method.

The repository processes this request and returns a Page<Book> object,  which contains the list of books for the current page as well as pagination metadata like total pages, total elements, current page number, and page size.

6. Creating the BookPage DTO

To ensure our GraphQL responses match the schema definition, we need to create a Data Transfer Object (DTO) called BookPage. This DTO acts as the crucial link between our Spring Data pagination results and the GraphQL type:

public class BookPage {
    private List<Book> content;
    private int totalPages;
    private long totalElements;
    private int number;
    private int size;

    public BookPage(Page<Book> page) {
        this.content = page.getContent();
        this.totalPages = page.getTotalPages();
        this.totalElements = page.getTotalElements();
        this.number = page.getNumber();
        this.size = page.getSize();
    }

    // Getters
}

This BookPage DTO is designed to accept a Page<Book> object in its constructor, where it extracts and organizes all the required data for our GraphQL response. By returning this DTO from the resolver, we ensure that the response matches our GraphQL schema exactly.

7. Cursor-Based Pagination

While page-based pagination serves well for typical applications, it faces limitations with extremely large datasets or infinite scrolling interfaces. Cursor-based pagination offers a more efficient alternative in these scenarios by using a different approach to track position.

Instead of relying on numeric page offsets, cursor pagination uses stable reference points – typically:

  • Encoded record IDs
  • Precise timestamps
  • Other unique, sequential identifiers

For our book example, we’ll implement this using book IDs as cursors. The client simply provides the last seen book ID, and the server returns all subsequent records after that point.

7.1. Updating the GraphQL Schema for Cursor-Based Pagination

Let’s update our GraphQL schema to support cursor-based pagination. We’ll add a new query and supporting types:

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

type BookEdge {
    node: Book
    cursor: String
}

type PageInfo {
    hasNextPage: Boolean
    endCursor: String
}

type BookConnection {
    edges: [BookEdge]
    pageInfo: PageInfo
}

type Query {
    booksByCursor(cursor: ID, limit: Int!): BookConnection
}

The BookEdge type structure allows us to maintain both the book data and its position in the sequence.

The BookConnection type encapsulates a list of edges along with a pageInfo object. The pageInfo provides useful metadata, such as whether more pages are available and the cursor needed to retrieve the next set of results.

7.2. Implementing the Cursor-Based GraphQL Query Resolver

Let’s now implement the resolver method that will handle our booksByCursor query. This resolver will process cursor-based pagination requests and return properly structured connection objects:

@QueryMapping
public BookConnection booksByCursor(@Argument Optional<Long> cursor, @Argument int limit) {
    List<Book> books;

    if (cursor.isPresent()) {
        books = bookRepository.findByIdGreaterThanOrderByIdAsc(cursor.get(), PageRequest.of(0, limit));
    } else {
        books = bookRepository.findAllByOrderByIdAsc(PageRequest.of(0, limit));
    }

    List<BookEdge> edges = books.stream()
      .map(book -> new BookEdge(book, book.getId().toString()))
      .collect(Collectors.toList());

    String endCursor = books.isEmpty() ? null : books.get(books.size() - 1).getId().toString();
    boolean hasNextPage = !books.isEmpty() && bookRepository.existsByIdGreaterThan(books.get(books.size() - 1).getId());

    PageInfo pageInfo = new PageInfo(hasNextPage, endCursor);

    return new BookConnection(edges, pageInfo);
}

In this method, we first examine if the client provided a cursor. When present, it queries for books with IDs greater than the decoded cursor value, maintaining ascending ID order. For initial requests without a cursor, it defaults to fetching the first set of records from the beginning of the collection, preserving the same ordering.

After retrieving the book records, the method transforms each one into a BookEdge object and combines the book data with its cursor. Next, we determine the endCursor by extracting the ID from the last book in the current result set.

To determine if additional pages exist, we perform a check for any books with IDs greater than the last record in our current results. The query existsByIdGreaterThan() avoids loading unnecessary data while providing the crucial boolean result we need.

7.3. Implementing the Supporting DTOs and Repository

To complete, we need to implement the supporting DTOs and enhance our repository with cursor-specific methods:

public class BookEdge {
    private Book node;
    private String cursor;

    public BookEdge(Book node, String cursor) {
        this.node = node;
        this.cursor = cursor;
    }

    // Getters
}

public class PageInfo {
    private boolean hasNextPage;
    private String endCursor;

    public PageInfo(boolean hasNextPage, String endCursor) {
        this.hasNextPage = hasNextPage;
        this.endCursor = endCursor;
    }

    // Getters
}

public class BookConnection {
    private List edges;
    private PageInfo pageInfo;

    public BookConnection(List edges, PageInfo pageInfo) {
        this.edges = edges;
        this.pageInfo = pageInfo;
    }

    // Getters
}

Finally, let’s extend our repository with cursor-specific query methods:

public interface BookRepository extends PagingAndSortingRepository<Book, Long> {
    List<Book> findByIdGreaterThanOrderByIdAsc(Long cursor, Pageable pageable);
    List<Book> findAllByOrderByIdAsc(Pageable pageable);
    boolean existsByIdGreaterThan(Long id);
}

8. Testing Pagination with JUnit

To ensure the pagination implementation is working correctly, we’ll create integration tests using JUnit.

8.1. Preparing Test Data

First, we initialize a consistent dataset before each test using the @BeforeEach setup method:

@BeforeEach
void setup() {
    bookRepository.deleteAll();

    for (int i = 1; i <= 50; i++) {
        Book book = new Book();
        book.setTitle("Test Book " + i);
        book.setAuthor("Test Author " + i);
        bookRepository.save(book);
    }
}

This guarantees that each test execution begins with a 50-dataset formatted book of records.

8.2. Testing Page-Based Pagination

To verify our page-based pagination, we’ll test the GraphQL endpoint using GraphQlTester. This instance is a testing utility that simplifies the process of executing GraphQL queries and validating responses in our integration tests.

Let’s write a method to test the /graphql endpoint and define a GraphQL query that asks for page 0 with 5 items per page:

@Test
void givenPageAndSize_whenQueryBooks_thenShouldReturnCorrectPage() {
    String query = "{ books(page: 0, size: 5) { content { id title author } totalPages totalElements number size } }";

    graphQlTester.document(query)
      .execute()
      .path("data.books")
      .entity(BookPageResponse.class)
      .satisfies(bookPage -> {
        assertEquals(5, bookPage.getContent().size());
        assertEquals(0, bookPage.getNumber());
        assertEquals(5, bookPage.getSize());
        assertEquals(50, bookPage.getTotalElements());
        assertEquals(10, bookPage.getTotalPages());
    });
}

This test checks if the first page returns 5 books and confirms that metadata property for total pages matches the 50-test-record dataset.

8.3. Testing Cursor-Based Pagination

To test the cursor-based pagination, we begin by executing a query for the first page of results without providing a cursor value. After receiving the first page response, we use the cursor obtained from the first page to request the next set of results.

This verifies that the cursor mechanism correctly maintains our position in the dataset and returns the subsequent items:

@Test
void givenCursorAndLimit_whenQueryBooksByCursor_thenShouldReturnNextBatch() {
    // First page
    String queryPage1 = "{ booksByCursor(limit: 5) { edges { node { id } cursor } pageInfo { endCursor hasNextPage } } }";
    
    BookConnectionResponse firstPage = graphQlTester.document(firstPageQuery)
      .execute()
      .path("data.booksByCursor")
      .entity(BookConnectionResponse.class)
      .get();

    assertEquals(5, firstPage.getEdges().size());
    assertTrue(firstPage.getPageInfo().isHasNextPage());
    assertNotNull(firstPage.getPageInfo().getEndCursor());

    // Second page using cursor
    String queryPage2 = "{ booksByCursor(cursor: \"" + firstPage.getPageInfo().getEndCursor() + "\", limit: 5) { edges { node { id } } pageInfo { hasNextPage } } }";

    graphQlTester.document(secondPageQuery)
      .execute()
      .path("data.booksByCursor")
      .entity(BookConnectionResponse.class)
      .satisfies(secondPage -> {
        assertEquals(5, secondPage.getEdges().size());
        assertTrue(secondPage.getPageInfo().isHasNextPage());
      });
}

9. Conclusion

In this article, we explored two approaches for implementing pagination in Spring Boot GraphQL APIs. Page-based pagination works well when dealing with smaller or finite datasets where the total number of items is known and doesn’t change frequently. On the other hand, cursor-based pagination is ideal for large datasets, infinite scrolling interfaces, and situations where data is frequently added or removed.

As always, the source code is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

Partner – Microsoft – NPI EA (cat = Baeldung)
announcement - icon

Azure Container Apps is a fully managed serverless container service that enables you to build and deploy modern, cloud-native Java applications and microservices at scale. It offers a simplified developer experience while providing the flexibility and portability of containers.

Of course, Azure Container Apps has really solid support for our ecosystem, from a number of build options, managed Java components, native metrics, dynamic logger, and quite a bit more.

To learn more about Java features on Azure Container Apps, visit the documentation page.

You can also ask questions and leave feedback on the Azure Container Apps GitHub page.

Partner – Microsoft – NPI EA (cat = Spring Boot)
announcement - icon

Azure Container Apps is a fully managed serverless container service that enables you to build and deploy modern, cloud-native Java applications and microservices at scale. It offers a simplified developer experience while providing the flexibility and portability of containers.

Of course, Azure Container Apps has really solid support for our ecosystem, from a number of build options, managed Java components, native metrics, dynamic logger, and quite a bit more.

To learn more about Java features on Azure Container Apps, visit the documentation page.

You can also ask questions and leave feedback on the Azure Container Apps GitHub page.

Partner – Orkes – NPI EA (cat = Spring)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Partner – Orkes – NPI EA (tag = Microservices)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Partner – MongoDB – NPI EA (tag=MongoDB)
announcement - icon

Traditional keyword-based search methods rely on exact word matches, often leading to irrelevant results depending on the user's phrasing.

By comparison, using a vector store allows us to represent the data as vector embeddings, based on meaningful relationships. We can then compare the meaning of the user’s query to the stored content, and retrieve more relevant, context-aware results.

Explore how to build an intelligent chatbot using MongoDB Atlas, Langchain4j and Spring Boot:

>> Building an AI Chatbot in Java With Langchain4j and MongoDB Atlas

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Microsoft – NPI (cat=Spring)
announcement - icon

Azure Container Apps is a fully managed serverless container service that enables you to build and deploy modern, cloud-native Java applications and microservices at scale. It offers a simplified developer experience while providing the flexibility and portability of containers.

Of course, Azure Container Apps has really solid support for our ecosystem, from a number of build options, managed Java components, native metrics, dynamic logger, and quite a bit more.

To learn more about Java features on Azure Container Apps, visit the documentation page.

You can also ask questions and leave feedback on the Azure Container Apps GitHub page.

eBook Jackson – NPI EA – 3 (cat = Jackson)