00:00

Spring Boot Pagination and Sorting

Last updated:

Real applications often store thousands or even millions of rows in a table. Loading all of them in one request wastes memory, slows down the database and makes the API response huge. Pagination solves this by returning the data in small pieces, and sorting makes sure those pieces arrive in a useful order.

Spring Boot, together with Spring Data JPA, gives you both features out of the box. In this tutorial you will build a small product catalog API and learn, step by step, how to page, sort and filter data the right way.

1. What Is Pagination?

Pagination means splitting a large result into smaller, fixed-size groups called pages. The client asks for one page at a time, and the server returns only the rows that belong to that page.

Think of a book: you read one page at a time instead of looking at every word at once. If a table has 95 products and the page size is 10, the data is split like this:

  • Page 0 → products 1 to 10
  • Page 1 → products 11 to 20
  • …
  • Page 9 → products 91 to 95 (the last page holds only 5 items)
95 products split into pages of 10Ninety-five rows are divided into ten pages of size 10. Page 0 holds rows 1 to 10, page 1 rows 11 to 20, and so on; page 9 holds only rows 91 to 95. Page numbers start at 0.Table: 95 products · page size = 10Page 01–10Page 111–20Page 221–30Page 331–40Page 441–50Page 551–60Page 661–70Page 771–80Page 881–90Page 991–95▲ PageRequest.of(2, 10)▲ only 5 rowspages are numbered from 0, so 10 pages are Page 0 … Page 9totalElements = 95 · totalPages = ceil(95 ÷ 10) = 10
Figure 1: pagination splits a result into fixed-size pages. Spring Data counts pages from 0, and the last page may be shorter.

In Spring Data, page numbers start at 0, not 1.

2. What Is Sorting?

Sorting arranges records in a defined order, based on one or more fields. Each field can be sorted in ascending order (A → Z, 1 → 9, oldest → newest) or descending order (Z → A, 9 → 1, newest → oldest).

Typical examples:

  • Products by price, cheapest first
  • Orders by date, newest first
  • Customers by last name, then by first name

Sorting matters even more when you paginate. Without a fixed order, the database may return rows in a different order on each request, and users could see the same item twice or miss one completely.

3. Why Use Pagination and Sorting?

BenefitWhat It Means
Faster responsesThe database reads and sends only a few rows per request.
Lower memory useThe application never holds the whole table in memory.
Smaller payloadsLess data travels over the network, which helps mobile clients.
Better user experienceUsers see results quickly and can move page by page.
Scalable APIsResponse time stays stable as the table keeps growing.

4. Key Interfaces and Classes

Spring Data provides a small set of types in the org.springframework.data.domain package. Once you know these, the rest of the topic is easy.

TypePurpose
PageableDescribes the request: which page, how many items, and in what order.
PageRequestThe ready-made implementation of Pageable. You create it with PageRequest.of(...).
SortDescribes the order: one or more fields, each with a direction.
Sort.OrderOne sort rule for a single field, such as "price descending".
Page<T>The result: the items of the current page plus totals (total items, total pages).
Slice<T>A lighter result: the items plus a "has next page" flag, without totals.

5. Project Setup

Create a Spring Boot 3 project with the Spring Web, Spring Data JPA and H2 Database dependencies. H2 is an in-memory database, so you can run the examples without installing anything.

pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Turn on SQL logging so you can see the queries Spring generates for each page:

application.properties
spring.datasource.url=jdbc:h2:mem:catalogdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

6. Creating the Entity

Our example uses a simple Product entity. The indexes on category and price help the database sort and filter quickly, which becomes important on big tables.

Product.java
package com.javabytechie.catalog.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;

import java.math.BigDecimal;
import java.time.LocalDate;

@Entity
@Table(name = "products", indexes = {
        @Index(name = "idx_product_category", columnList = "category"),
        @Index(name = "idx_product_price", columnList = "price")
})
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String category;
    private BigDecimal price;
    private LocalDate addedOn;

    protected Product() {
        // required by JPA
    }

    public Product(String name, String category, BigDecimal price, LocalDate addedOn) {
        this.name = name;
        this.category = category;
        this.price = price;
        this.addedOn = addedOn;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getCategory() { return category; }
    public BigDecimal getPrice() { return price; }
    public LocalDate getAddedOn() { return addedOn; }
}

To have something to page through, load some sample rows when the application starts:

SampleDataLoader.java
package com.javabytechie.catalog.config;

import com.javabytechie.catalog.entity.Product;
import com.javabytechie.catalog.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Component
public class SampleDataLoader implements CommandLineRunner {

    private final ProductRepository productRepository;

    public SampleDataLoader(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public void run(String... args) {
        String[] categories = {"Books", "Laptops", "Phones", "Toys"};
        List<Product> products = new ArrayList<>();

        for (int i = 1; i <= 50; i++) {
            String category = categories[i % categories.length];
            BigDecimal price = BigDecimal.valueOf(100L + (i * 37L) % 900);
            products.add(new Product("Product " + i, category, price,
                    LocalDate.of(2025, 1, 1).plusDays(i)));
        }

        productRepository.saveAll(products);
    }
}

7. Creating the Repository

JpaRepository already includes paging and sorting support, so a one-line interface is enough to get findAll(Pageable) and findAll(Sort).

You can also add your own derived query methods. If you add a Pageable or Sort parameter to them, Spring Data applies the paging and ordering for you.

ProductRepository.java
package com.javabytechie.catalog.repository;

import com.javabytechie.catalog.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Paged results for one category, with total counts
    Page<Product> findByCategory(String category, Pageable pageable);

    // Paged results without a count query
    Slice<Product> findByPriceLessThan(BigDecimal maxPrice, Pageable pageable);

    // Sorted results, no paging
    List<Product> findByCategory(String category, Sort sort);

    // Fixed limit and order written into the method name
    List<Product> findTop5ByOrderByPriceDesc();
}

8. Pagination Using Pageable

Build a Pageable with PageRequest.of(page, size) and pass it to the repository. Spring Data adds the correct LIMIT and OFFSET to the SQL automatically.

ProductService.java
public Page<Product> getProducts(int page, int size) {
    Pageable pageable = PageRequest.of(page, size);
    return productRepository.findAll(pageable);
}

For page = 2 and size = 10, Hibernate runs two queries: one to fetch the rows and one to count the total.

Console output
select p.id, p.added_on, p.category, p.name, p.price
from products p
offset 20 rows fetch first 10 rows only

select count(p.id)
from products p

The offset is calculated as page × size, so page 2 skips the first 20 rows. The exact SQL syntax depends on your database, but the idea is the same everywhere.

How a paged request flows through Spring BootThe client sends GET /api/products?page=2&size=10. The service builds PageRequest.of(2, 10) and calls the repository. Hibernate runs a data query with offset 20 and fetch first 10 rows, and a count query. The rows and the total are combined into a Page object, which becomes the JSON response with content and paging details.ClientGET /api/products?page=2&size=10Controller / ServicePageRequest.of(2, 10)ProductRepositoryfindAll(pageable)Hibernate runs two queries① select … from products p offset 20 rows fetch first 10 rows onlyrows 21–30 (offset = page × size = 2 × 10)② select count(p.id) from products ptotal = 95 (needed for totalPages)DBproductsPage<Product>content = 10 items · number = 2 · totalElements = 95 · totalPages = 10 · hasNext = trueJSON response: content + paging details
Figure 2: one paged request becomes two SQL queries, one for the rows and one for the total count, and Spring Data wraps both in a Page.

9. Understanding the Page Object

A Page holds the rows of the current page plus information about the full result. These are the methods you will use most:

Anatomy of a Page objectA Page has two parts: content, the list of items on the current page, and metadata such as the page number, page size, number of elements, total elements, total pages and whether there are next or previous pages.Page<Product> (request: page 2, size 10)getContent()Product 21Product 22…Product 30getNumber()2getSize()10getNumberOfElements()10getTotalElements()95getTotalPages()10hasNext() / hasPrevious()true / true
Figure 3: a Page carries the items for this page and the numbers a client needs to draw page links.
MethodReturns
getContent()The list of items on the current page
getNumber()The current page number (0-based)
getSize()The requested page size
getNumberOfElements()How many items this page actually contains
getTotalElements()The total number of matching rows
getTotalPages()The total number of pages
isFirst() / isLast()Whether this is the first or last page
hasNext() / hasPrevious()Whether another page exists after or before this one
map(...)A new Page with each item converted, for example from entity to DTO
PageInfoExample.java
Page<Product> result = productRepository.findAll(PageRequest.of(0, 10));

System.out.println("Items on this page : " + result.getNumberOfElements());
System.out.println("Current page       : " + result.getNumber());
System.out.println("Total items        : " + result.getTotalElements());
System.out.println("Total pages        : " + result.getTotalPages());
System.out.println("Has next page?     : " + result.hasNext());

result.getContent().forEach(product -> System.out.println(product.getName()));

10. Sorting Using Sort

The Sort class describes the order of the results. The field names are the Java property names of the entity (for example addedOn), not the database column names.

10.1 Sort by One Field

SortExamples.java
// Cheapest first
List<Product> byPriceAsc = productRepository.findAll(Sort.by("price"));

// Most expensive first
List<Product> byPriceDesc = productRepository.findAll(Sort.by("price").descending());

// Same result, using the Direction enum
List<Product> byPriceDesc2 = productRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));

10.2 Sort by Multiple Fields

When two rows have the same value in the first field, the second field decides their order.

SortExamples.java
// Category A → Z, then price high → low inside each category
Sort sort = Sort.by("category").ascending()
        .and(Sort.by("price").descending());

List<Product> products = productRepository.findAll(sort);

10.3 Sort with Sort.Order

Sort.Order gives you finer control, such as case-insensitive sorting:

SortExamples.java
Sort sort = Sort.by(
        Sort.Order.asc("name").ignoreCase(),
        Sort.Order.desc("addedOn")
);

List<Product> products = productRepository.findAll(sort);

11. Pagination and Sorting Together

In real APIs, the client usually sends the page number, page size, sort field and sort direction. This section builds a complete, clean endpoint for that.

11.1 Response DTOs

Returning entities directly exposes your database structure. Use small DTO records instead, plus one reusable wrapper for paged responses:

ProductDto.java
package com.javabytechie.catalog.dto;

import com.javabytechie.catalog.entity.Product;

import java.math.BigDecimal;

public record ProductDto(Long id, String name, String category, BigDecimal price) {

    public static ProductDto from(Product product) {
        return new ProductDto(
                product.getId(),
                product.getName(),
                product.getCategory(),
                product.getPrice()
        );
    }
}
PagedResponse.java
package com.javabytechie.catalog.dto;

import org.springframework.data.domain.Page;

import java.util.List;

public record PagedResponse<T>(
        List<T> content,
        int page,
        int size,
        long totalElements,
        int totalPages,
        boolean first,
        boolean last
) {

    public static <T> PagedResponse<T> from(Page<T> page) {
        return new PagedResponse<>(
                page.getContent(),
                page.getNumber(),
                page.getSize(),
                page.getTotalElements(),
                page.getTotalPages(),
                page.isFirst(),
                page.isLast()
        );
    }
}

11.2 Service Layer

The service checks the input, builds the Sort and Pageable, and converts each entity to a DTO with Page.map().

ProductService.java
package com.javabytechie.catalog.service;

import com.javabytechie.catalog.dto.PagedResponse;
import com.javabytechie.catalog.dto.ProductDto;
import com.javabytechie.catalog.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class ProductService {

    private static final int MAX_PAGE_SIZE = 50;

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public PagedResponse<ProductDto> getProducts(int page, int size,
                                                String sortBy, String direction) {
        int safePage = Math.max(page, 0);
        int safeSize = Math.min(Math.max(size, 1), MAX_PAGE_SIZE);

        Sort.Direction dir = "desc".equalsIgnoreCase(direction)
                ? Sort.Direction.DESC
                : Sort.Direction.ASC;

        // "id" as a second sort key keeps the order stable when values repeat
        Sort sort = Sort.by(dir, sortBy).and(Sort.by("id"));

        Pageable pageable = PageRequest.of(safePage, safeSize, sort);

        Page<ProductDto> result = productRepository.findAll(pageable)
                .map(ProductDto::from);

        return PagedResponse.from(result);
    }
}

11.3 Controller

ProductController.java
package com.javabytechie.catalog.controller;

import com.javabytechie.catalog.dto.PagedResponse;
import com.javabytechie.catalog.dto.ProductDto;
import com.javabytechie.catalog.service.ProductService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public PagedResponse<ProductDto> getProducts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String direction) {

        return productService.getProducts(page, size, sortBy, direction);
    }
}

11.4 Testing the Endpoint

Start the application and call the API. This request asks for the first page of 3 products, most expensive first:

Terminal
curl "http://localhost:8080/api/products?page=0&size=3&sortBy=price&direction=desc"

The response contains the products along with all the paging details the client needs to build "Previous" and "Next" buttons. With the sample data from section 6, it looks like this:

Response
{
  "content": [
    { "id": 24, "name": "Product 24", "category": "Books", "price": 988 },
    { "id": 48, "name": "Product 48", "category": "Books", "price": 976 },
    { "id": 23, "name": "Product 23", "category": "Toys", "price": 951 }
  ],
  "page": 0,
  "size": 3,
  "totalElements": 50,
  "totalPages": 17,
  "first": true,
  "last": false
}

12. Pageable as a Controller Parameter

Spring MVC can also build the Pageable for you. Just declare it as a method parameter, and Spring reads the page, size and sort values from the URL.

ProductController.java
@GetMapping("/by-category")
public PagedResponse<ProductDto> getByCategory(
        @RequestParam String category,
        @PageableDefault(size = 10, sort = "price", direction = Sort.Direction.ASC)
        Pageable pageable) {

    Page<ProductDto> result = productRepository
            .findByCategory(category, pageable)
            .map(ProductDto::from);

    return PagedResponse.from(result);
}

@PageableDefault sets the values used when the client sends nothing. Here are a few example URLs:

Example requests
# Defaults: page 0, 10 items, sorted by price ascending
GET /api/products/by-category?category=Books

# Second page with 5 items
GET /api/products/by-category?category=Books&page=1&size=5

# Sort by price descending, then by name ascending
GET /api/products/by-category?category=Books&sort=price,desc&sort=name,asc

The sort parameter uses the format field,direction. Repeat it to sort by more than one field. If you leave out the direction, ascending is used.

13. Global Pagination Settings

Spring Boot lets you change the default paging behavior for the whole application in application.properties. These settings apply whenever Pageable is resolved from a request.

application.properties
# Page size used when the client does not send one (default: 20)
spring.data.web.pageable.default-page-size=10

# Largest page size a client may request (default: 2000)
spring.data.web.pageable.max-page-size=100

# Let clients start counting pages at 1 instead of 0 (default: false)
spring.data.web.pageable.one-indexed-parameters=true

# Rename the request parameters if needed
spring.data.web.pageable.page-parameter=page
spring.data.web.pageable.size-parameter=size
spring.data.web.sort.sort-parameter=sort

If a client asks for more than max-page-size, Spring silently reduces the size to that limit. This simple setting protects your API from requests like size=1000000.

14. Page vs Slice vs List

A repository method can return one of three types when it receives a Pageable. Choose based on what your screen or client really needs.

Return TypeCount Query?Knows Total Pages?Best For
Page<T>YesYesTables with page numbers such as "Page 3 of 17"
Slice<T>NoNo, only hasNext()"Load more" buttons and infinite scrolling
List<T>NoNoWhen you only need the rows themselves

A Slice works by fetching one extra row (size + 1). If that extra row exists, there is a next page. This avoids the COUNT query, which can be slow on very large tables.

Page versus SlicePage runs the data query plus a count query and knows the total number of pages, which suits numbered page links. Slice asks for size plus one rows and runs no count query; if the extra row comes back there is a next page, which suits load-more buttons and infinite scrolling.Page<T>Slice<T>query 1: 10 rowsoffset 20 · fetch 10query 2: COUNT(*)scans the matching rowsknows: totalElements, totalPages◀ 1 2 [3] 4 … 10 ▶query 1: 11 rows (size + 1)offset 20 · fetch 11extra11th row found → hasNext() = trueno COUNT query, no totalLoad more ↓
Figure 4: Page pays for a COUNT query to know the total; Slice fetches one extra row just to learn whether more data exists.
SliceExample.java
Slice<Product> slice = productRepository.findByPriceLessThan(
        BigDecimal.valueOf(500), PageRequest.of(0, 10, Sort.by("price")));

slice.getContent().forEach(p -> System.out.println(p.getName()));

if (slice.hasNext()) {
    Slice<Product> next = productRepository.findByPriceLessThan(
            BigDecimal.valueOf(500), slice.nextPageable());
}

15. Pagination with Custom Queries

Paging also works with @Query. Spring Data creates the count query for simple JPQL by itself, but for complex queries it is safer to write the count query yourself with countQuery.

ProductRepository.java
// JPQL: search products by part of their name
@Query(value = "SELECT p FROM Product p "
             + "WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))",
       countQuery = "SELECT COUNT(p) FROM Product p "
             + "WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))")
Page<Product> searchByName(@Param("keyword") String keyword, Pageable pageable);

// Native SQL: the count query is required for paging
@Query(value = "SELECT * FROM products WHERE category = :category",
       countQuery = "SELECT COUNT(*) FROM products WHERE category = :category",
       nativeQuery = true)
Page<Product> findByCategoryNative(@Param("category") String category, Pageable pageable);

Do not write ORDER BY inside a paged query. Let the Sort inside the Pageable control the order, otherwise the two can conflict.

16. Validating Sort Fields

If a client sends a field that does not exist, such as sortBy=colour, Spring Data throws a PropertyReferenceException and the client sees a 500 error. Accepting any field name also lets clients sort by columns you never meant to expose.

The fix is simple: keep a list of allowed fields and reject everything else with a clear 400 Bad Request.

ProductService.java
private static final Set<String> ALLOWED_SORT_FIELDS =
        Set.of("id", "name", "category", "price", "addedOn");

private void validateSortField(String sortBy) {
    if (!ALLOWED_SORT_FIELDS.contains(sortBy)) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Cannot sort by '" + sortBy + "'. Allowed fields: " + ALLOWED_SORT_FIELDS);
    }
}

Call validateSortField(sortBy) at the start of getProducts(...), before building the Sort.

17. Large Tables and Keyset Scrolling

Offset pagination has one weakness: to show page 10,000, the database must still read and skip all the rows before it. On very large tables, the deep pages become slow.

Offset pagination versus keyset paginationWith offset pagination, reading page 2000 means the database walks past 20,000 rows and throws them away before returning 10. With keyset pagination, the query asks for rows where id is greater than the last id seen, so an index jumps straight to the right place and reads only 10 rows.OFFSET: page 2000, size 10select … order by id offset 20000 rows fetch first 10 rows onlyreads and discards 20,000 rows ✘returns 10deeper page → more rows skipped → slowerKEYSET: rows after the last id seenselect … where id > 20000 order by id fetch first 10 rows onlyindex on idB-treeseek straight to id 20001 ✔reads only 10 rows
Figure 5: offset pagination gets slower on deep pages; keyset (seek) pagination stays fast because the index jumps straight to the next rows.

Keyset pagination (also called seek or cursor pagination) avoids this. Instead of "skip N rows", it asks for "rows after the last one I saw", which an index can answer quickly. Spring Data 3.1 and later support this through ScrollPosition and Window:

ProductRepository.java
// Returns up to 10 products after the given position, ordered by id
Window<Product> findFirst10ByOrderByIdAsc(ScrollPosition position);
KeysetExample.java
Window<Product> window = productRepository.findFirst10ByOrderByIdAsc(ScrollPosition.keyset());

while (!window.isEmpty()) {
    window.forEach(product -> System.out.println(product.getName()));

    if (!window.hasNext()) {
        break;
    }
    // Continue right after the last product of the current window
    window = productRepository.findFirst10ByOrderByIdAsc(
            window.positionAt(window.size() - 1));
}

Keyset scrolling is ideal for infinite feeds and batch jobs. The trade-off is that users cannot jump straight to "page 500", so normal offset paging is still the better choice for small and medium tables.

18. Best Practices

  • Paginate every list endpoint. A table that is small today may hold millions of rows next year.
  • Set a maximum page size with max-page-size or by clamping the value in your service.
  • Always sort paged data, and add a unique field such as id as the last sort key so the order never changes between requests.
  • Allow only known sort fields to avoid errors and hide internal columns.
  • Return DTOs, not entities, and wrap them in a clear response object with paging metadata.
  • Add indexes on columns that are often used for sorting and filtering.
  • Use Slice when totals are not needed to skip the extra count query.
  • Consider keyset scrolling for very large tables or infinite scrolling feeds.

19. Common Mistakes to Avoid

  • Assuming pages start at 1. Spring Data starts at 0 unless you enable one-indexed-parameters.

  • Paging without sorting. The database may return rows in a random order, so items repeat or go missing across pages.

  • Loading everything and paging in Java. Calling findAll() and then using subList() defeats the whole purpose.

  • Using database column names in Sort. JPQL sorting expects entity property names such as addedOn, not added_on.

  • Forgetting countQuery in native queries. Without it, paging a native query can fail or return wrong totals.

  • Fetching collections with JOIN FETCH while paging. Hibernate then pages in memory and logs a warning. Fetch the IDs first, or use an entity graph on a single-valued relation.

  • Sending the whole Page object as JSON. Its structure is internal and may change between versions. Use your own DTO instead.

20. Conclusion

Pagination and sorting are must-have features for any API that returns lists. With Spring Data JPA, you only need a Pageable to page your data, a Sort to order it, and a Page or Slice to read the result.

For production APIs, add a few safety steps: limit the page size, allow only known sort fields, keep the order stable with a unique tie-breaker, and return clean DTOs. For huge tables, switch to keyset scrolling with Window and ScrollPosition. With these habits, your APIs stay fast as your data grows.