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
Lesson 9 of 11
1d streakThe 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
Page<T> (Spring Data)
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
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI Path | /api/v1/orders, /api/v2/orders | Visible in browser, straightforward routing and caching | URL pollution, duplicate routes |
| Custom Header | X-API-Version: 2 | Clean resource URLs, easy client defaults | Cannot test via raw browser address bar |
| Media Type (Accept) | Accept: application/vnd.company.v2+json | Strictly REST-compliant content negotiation | Complex 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.
- 01Client sends POST /api/orders with Idempotency-Key: 7b8c9d0e.
- 02Server checks an idempotency store (Redis or database table) for the key.
- 03If the key is currently PROCESSING, return 409 Conflict.
- 04If the key has a STORED_RESPONSE, return the saved status and body immediately without re-executing business logic.
- 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
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.
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);
}
}Synthesis & Production Verification
Deliberate Failure Condition
- Break: Retry a timed-out POST /api/orders twice while the server processed each attempt
- Observe: Three identical orders for one user action and support tickets nobody can reproduce
- 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
Key Takeaway
Bound collections with Pageable and Slice to protect heap memory, evolve APIs additively, and deduplicate mutations using Idempotency-Key headers.
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
Lesson Primary References
Supplementary Module Resources