Foundation · Build

Pagination, Versioning & Idempotency Keys

Keep list endpoints cheap with pages and slices, evolve the contract without breaking clients, and make retried POSTs safe by design.

Primary source: Spring Data Commons: Web Support and Pagination

30 min

Lesson 9 of 11

1d streak

The Architectural Question

Unpaged lists exhaust memory and latency as data grows, one breaking field rename strands every deployed client, and network retries silently create duplicate orders.

What this guide proves

  • Bound every collection response with page, size, and sort parameters
  • Prefer additive evolution; version only genuinely breaking changes
  • Accept Idempotency-Key headers to deduplicate client retries

Bound every collection query with Pageable and Slice

Returning unbounded collections (e.g., List<Order>) is one of the most common causes of production OutOfMemoryErrors. As database records grow from hundreds to millions, a single unpaged findAll() query can exhaust the JVM heap.

Architectural Boundary · Do Not Confuse

Concept A

Page<T> (Spring Data)

Concept B

Slice<T> (Spring Data)

Page<T> executes two queries: one for the requested slice of data and a separate COUNT(*) query to determine the total number of pages. Slice<T> queries only for limit + 1 items to verify if a next page exists, completely eliminating the expensive COUNT query. For infinite scrolling or large datasets, Slice is significantly more performant.

API Evolution: URI Path vs Headers vs Media Types

StrategyExampleProsCons
URI Path/api/v1/orders, /api/v2/ordersVisible in browser, straightforward routing and cachingURL pollution, duplicate routes
Custom HeaderX-API-Version: 2Clean resource URLs, easy client defaultsCannot test via raw browser address bar
Media Type (Accept)Accept: application/vnd.company.v2+jsonStrictly REST-compliant content negotiationComplex client configuration, difficult proxy caching

Prevent duplicate mutations with Idempotency-Key headers

In distributed networks, clients often retry timed-out POST requests. If the server successfully processed the initial attempt, retrying blindly creates duplicate orders or charges customers twice. An Idempotency-Key header prevents this.

  1. 01Client sends POST /api/orders with Idempotency-Key: 7b8c9d0e.
  2. 02Server checks an idempotency store (Redis or database table) for the key.
  3. 03If the key is currently PROCESSING, return 409 Conflict.
  4. 04If the key has a STORED_RESPONSE, return the saved status and body immediately without re-executing business logic.
  5. 05If the key is NEW, acquire a lock, execute the order creation, store the response in the idempotency table, and return 201 Created.

Paged controller with stable sorting and idempotency guard

OrderQueryController.java
package com.example.orders.api;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderQueryController {

    @GetMapping
    public ResponseEntity<Slice<OrderSummary>> getOrders(
            @PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
        // service returns Slice to avoid expensive COUNT queries
        Slice<OrderSummary> slice = orderService.findOrderSummaries(pageable);
        return ResponseEntity.ok(slice);
    }
}

record OrderSummary(Long id, String status, double total) {}

Active Recall Checkpoint

Why should you prefer additive schema changes over incrementing API versions for small field additions?

Hint: Consider the cost of supporting multiple concurrent API versions.

Show answer & explanation

Additive changes (adding optional fields) are non-breaking for well-behaved JSON clients. Incrementing the API version forces code duplication, separate deployment tracks, and client migration friction.

Operational rules for collection endpoints

Use it when

Always apply @PageableDefault to collection endpoints and default to Slice<T> when total page counts are not explicitly required by the UI.

Reconsider when

Do not use Page<T> on tables with millions of rows without measuring the performance of the generated SELECT COUNT(*) query.

Working Implementation

Code Walkthrough

Page the task list with Pageable, then add an idempotency key check around POST so repeating the same key returns the first result.

PaginationVersioningIdempotencyLab.java
package com.example.webadvanced;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RequestMapping("/api/v1/orders")
public class PaginationVersioningIdempotencyLab {

    public record OrderRecord(String id, String description, long amountCents) {}
    private final Map<String, OrderRecord> orders = new ConcurrentHashMap<>();
    private final Set<String> processedKeys = ConcurrentHashMap.newKeySet();

    // 1. Pagination: Spring Data Pageable query handling
    @GetMapping
    public Page<OrderRecord> getOrders(Pageable pageable) {
        List<OrderRecord> list = new ArrayList<>(orders.values());
        int start = (int) pageable.getOffset();
        int end = Math.min(start + pageable.getPageSize(), list.size());
        List<OrderRecord> subList = start > list.size() ? List.of() : list.subList(start, end);
        return new PageImpl<>(subList, pageable, list.size());
    }

    // 2. Idempotency Key deduplication
    @PostMapping
    public ResponseEntity<?> createOrder(
        @RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
        @RequestBody OrderRecord order
    ) {
        if (idempotencyKey != null && !processedKeys.add(idempotencyKey)) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("Duplicate request: Idempotency-Key already processed");
        }
        orders.put(order.id(), order);
        return ResponseEntity.status(HttpStatus.CREATED).body(order);
    }
}
Pageable injects page, size, and sort parameters cleanly into Spring MVC endpoints.
Idempotency-Key headers prevent accidental double-billing during network retries.
API versioning in the URI path (/api/v1/...) provides explicit contract boundaries for consumers.

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Retry a timed-out POST /api/orders twice while the server processed each attempt
  2. Observe: Three identical orders for one user action and support tickets nobody can reproduce
  3. Repair: Exactly-once needs client cooperation: a stable idempotency key lets the server recognize repeats and return the original outcome.

Production Acceptance Criteria

  • Serve paged results with stable ordering and correct total counts
  • Add a response field without breaking recorded client fixtures
  • Replay one POST with the same key and receive the original response

Architectural & Technical Review

Core Interview Questions

Q01What is the performance advantage of Slice<T> over Page<T>?Show answer

Slice queries for limit + 1 items to verify next-page existence, completely eliminating the expensive COUNT(*) query required by Page.

Q02Which API versioning strategy is best suited for enterprise web services?Show answer

URI path versioning (/v1/orders) is the most explicit and browser-friendly, while additive non-breaking changes should be prioritized before bumping versions.

Q03How does an Idempotency-Key header protect POST operations?Show answer

The server records the key and cached response in a shared store; retried client requests return the cached response without re-executing business logic.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Returning unbounded findAll() collections — Unbounded collections exhaust database connections and trigger JVM OutOfMemoryErrors as data volume expands.
  • Pitfall: Incrementing API versions for backward-compatible additions — Adding optional fields is non-breaking; creating new versions for every change fractures client ecosystems unnecessarily.

Primary Documentation & Specifications

Supplementary Module Resources