Skip to main content
java interview engineering first principles to system design

API Design: REST, Versioning, and Idempotency

10 min read Chapter 25 of 32
Summary

This section focuses on designing RESTful APIs with...

This section focuses on designing RESTful APIs with emphasis on resource modeling, versioning strategies, idempotency, and error handling. Key concepts include REST principles where resources are nouns (e.g., users, payments) and operations use HTTP verbs (GET, POST, PUT, DELETE). Resource modeling involves nested paths like /users/{userId}/posts and query parameters for filtering. API versioning is implemented via URL paths, headers, or query parameters, each with trade-offs in explicitness and decoupling. Idempotency is critical for retry safety, with POST requests requiring idempotency keys to prevent duplicates, while PUT and DELETE are inherently idempotent. Error handling uses HTTP status codes such as 200 OK, 201 Created, 400 Bad Request, and 429 Too Many Requests for rate limiting. Pagination techniques like limit/offset and cursor-based methods are compared, with cursor-based offering consistency at the cost of complexity. Rate limiting enforces per-user quotas with 429 responses. Java 21+ features are integrated: Records for immutable DTOs (e.g., PaymentRequest, PaymentResponse), virtual threads for concurrency in API endpoints, and complexity analysis for operations like O(1) cache lookups and O(n) task processing. A verification exercise challenges designing a payment processing API with idempotency keys and retry logic in 10 minutes. Trade-offs are stated explicitly, such as REST's simplicity versus GraphQL's flexibility.

API Design: REST, Versioning, and Idempotency

API design is a critical component of system architecture, particularly in RESTful services where clarity, scalability, and reliability are paramount. Building upon the system design fundamentals introduced in Chapter 6—such as capacity estimation and data modeling—this section focuses on crafting RESTful APIs with precise resource modeling, robust versioning strategies, and idempotency mechanisms. Using Java 21+ features like Records for immutable data carriers and virtual threads for concurrency, you will learn to implement APIs that handle errors with appropriate HTTP status codes and scale efficiently. The verification exercise challenges you to design a payment processing API with idempotency keys and retry logic within 10 minutes, applying all concepts discussed.

REST Principles and Resource Modeling

REST (Representational State Transfer) is an architectural style for designing networked applications that emphasizes resources as nouns, uses HTTP verbs (GET, POST, PUT, DELETE) for operations, maintains stateless communication, and provides a uniform interface. Resource modeling in REST APIs involves defining endpoints using nouns for resources, with nested structures for hierarchy and query parameters for filtering. For example, in a social media API, resources could include users and posts, with endpoints like /users/{userId}/posts to access posts of a specific user, and query parameters such as ?status=active for filtering.

The uniform interface constraint ensures APIs expose a consistent set of operations through HTTP methods and resource identifiers. This simplifies client interactions by standardizing how resources are identified and manipulated. In practice, REST APIs are stateless, meaning each request contains all necessary information without relying on server-side session storage, which enhances scalability. For instance, the UrlShortenerAPI from Chapter 6 uses Records for request and response models, demonstrating how to structure endpoints like POST /shorten and GET /{shortUrl} with immutable data.

// Java 21+ Records for payment API request and response with idempotency key
public record PaymentRequest(String amount, String currency, String idempotencyKey) {}
public record PaymentResponse(String paymentId, String status, String timestamp) {}

// API endpoint using virtual threads for concurrency
import java.util.concurrent.*;
public class PaymentAPI {
    private static final ConcurrentHashMap<String, PaymentResponse> idempotencyCache = new ConcurrentHashMap<>();
    
    public PaymentResponse processPayment(PaymentRequest request) {
        // Check idempotency key
        if (idempotencyCache.containsKey(request.idempotencyKey())) {
            return idempotencyCache.get(request.idempotencyKey());
        }
        // Simulate payment processing with virtual thread
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future = executor.submit(() -> {
                // Payment logic here
                return new PaymentResponse("pay_123", "success", java.time.Instant.now().toString());
            });
            PaymentResponse response = future.get();
            idempotencyCache.put(request.idempotencyKey(), response);
            return response;
        } catch (Exception e) {
            throw new RuntimeException("Payment failed", e);
        }
        // Time Complexity: O(1) average for cache lookup, O(n) for processing with n tasks; Space Complexity: O(k) for cache where k is unique keys.
    }
}

This code exemplifies RESTful design with resource modeling using Records for immutable data transfer objects (DTOs) and virtual threads for handling concurrent requests. The time complexity is O(1) average for cache lookups and O(n) for processing, with space complexity O(k) for the cache, aligning with the complexity analysis provided in the following table.

OperationTime ComplexitySpace ComplexityNotes
API request handling (with virtual threads)O(n) for n concurrent tasksO(n) for thread storage, ~2KB per virtual threadAssumes I/O-bound tasks; worst-case O(n) if CPU-bound.
Idempotency key lookup in cacheO(1) average, O(n) worst-case with collisionsO(k) for k cached keysUsing HashMap for cache; Record keys ensure proper hashing.
Pagination with limit/offsetO(n) for scanning offsetO(1) extra space for offsetMay miss updates if data changes during pagination.
Pagination with cursorO(n) for traversing to cursorO(1) extra space for cursor tokenProvides consistency but requires cursor management.
Rate limiting per userO(1) for counter updatesO(u) for u users’ countersUsing distributed cache; time includes network latency.

This table compares time and space complexities for common API operations, such as request handling with virtual threads, idempotency key caching, pagination, and rate limiting, using Big-O notation. For pagination, cursor-based methods offer consistency across updates at the cost of increased complexity, while limit/offset is simpler but may miss updates.

API Versioning Strategies

API versioning is the practice of managing changes to an API by providing multiple versions to ensure backward compatibility. It can be implemented through methods like URL path (e.g., /v1/users), headers (e.g., Accept: application/vnd.api+json;version=1), or query parameters (e.g., ?version=1). Each approach has trade-offs in explicitness and decoupling, as detailed in the following matrix.

AspectURL Versioning (/v1/resource)Header Versioning (Accept header)Query Parameter Versioning (?version=1)
ExplicitnessHigh: version clear in URILow: hidden in headersMedium: visible in query
DecouplingLow: URI changes with versionHigh: URI remains stableMedium: URI stable but query changes
Client ComplexityLow: easy to parseHigh: requires header handlingMedium: requires query parsing
CachingMay break if version in pathBetter as URI unchangedSimilar to URL versioning
Best Use CaseWhen explicit versioning is neededFor API evolution without URI changesFor simple versioning with query support

URL versioning makes versions explicit in the URI but can lead to URI pollution, while header versioning keeps URIs clean but adds complexity to client requests. For example, in a payment API, using /v1/payments ensures clarity but may require URI updates for new versions, whereas header versioning allows clients to specify versions without altering the URI.

Idempotency and Error Handling

Idempotency is a property of an operation where performing it multiple times yields the same result as performing it once, critical for retry safety in APIs. PUT and DELETE are inherently idempotent, while POST requires idempotency keys to prevent duplicate creations, commonly used in payment processing APIs. An idempotency key is a unique identifier sent with a POST request to ensure idempotency by allowing the server to recognize and avoid duplicate processing.

Error handling in REST APIs should use appropriate HTTP status codes and provide descriptive error messages in the response body for debugging. Standard codes include 200 OK for successful retrieval, 201 Created for resource creation, 400 Bad Request for invalid client input, 404 Not Found for non-existent resources, 500 Internal Server Error for server-side failures, and 503 Service Unavailable for overloaded services. For instance, in the payment API code, exceptions are caught and rethrown as runtime exceptions, but in production, more granular error handling with specific status codes would be implemented.

Common failure modes in API design include not handling idempotency keys for POST requests, leading to duplicate payments, or incorrect HTTP status codes, such as using 500 for client errors. The following checklist outlines these pitfalls and mitigation strategies.

Common failure modes in API design:

  1. Not handling idempotency keys for POST requests, leading to duplicate payments.
  2. Incorrect HTTP status codes, e.g., using 500 for client errors.
  3. Missing pagination for large datasets, causing performance issues.
  4. Ignoring rate limiting, resulting in API abuse and downtime.
  5. Poor resource modeling, such as using verbs in URIs.
  6. Not validating input parameters, leading to security vulnerabilities.
  7. Lack of error handling for edge cases like network timeouts.
  8. Using mutable DTOs in concurrent environments without synchronization.
  9. Forgetting to include versioning strategy, breaking backward compatibility.
  10. Not analyzing time and space complexity for scalability. Mitigation strategies: Use Records for immutability, implement idempotency keys, enforce rate limits, and test thoroughly.

To address these, use Java 21+ Records for immutable DTOs, as shown in the payment API example, which ensures thread-safety and reduces GC pressure. The memory layout for API components in Java 21+ highlights efficiencies: PaymentRequest Records store components directly in object headers with fixed layout, reducing overhead compared to POJOs, and virtual threads allocate stacks as on-heap chunks of ~2KB, enabling millions of threads for I/O-bound tasks.

Pagination and Rate Limiting

Pagination is a technique to partition large datasets into manageable chunks for retrieval, using approaches like limit/offset (simple but may miss updates) or cursor-based (more complex but consistent). Cursor-based pagination uses a pointer (cursor) to a specific position in the dataset, enabling consistent retrieval across updates without the offset limitations of limit/offset.

Rate limiting is a control mechanism to restrict the number of API requests a user can make within a time period, typically enforced with per-user quotas and returning HTTP status 429 Too Many Requests with a Retry-After header. Algorithms can use token buckets or leaky buckets, with per-user counters stored in a distributed cache for scalability, as referenced in the capacity estimation from CH6-S1 for calculating queries per second (QPS).

Design Patterns and Trade-offs

When designing APIs, trade-offs must be stated explicitly. For example, REST APIs provide simplicity and statelessness at the cost of flexibility compared to GraphQL, which offers a single endpoint with queries but may have over-fetching issues. The following matrix compares these approaches.

AspectREST APIGraphQL
SimplicityHigh: uses standard HTTPLow: requires GraphQL schema
StatelessnessHigh: no server sessionMedium: may have stateful connections
FlexibilityLow: fixed endpointsHigh: single endpoint with queries
PerformanceGood for simple queriesCan be inefficient with over-fetching
Use CaseStandard CRUD operationsComplex data fetching needs

REST is ideal for standard CRUD operations, while GraphQL suits complex data fetching needs, but this discussion focuses on REST principles as per logic constraints.

Interview pattern template for designing APIs:

  1. Clarify Requirements: Ask about functional (e.g., payment processing) and non-functional (scalability, latency) needs.
  2. Model Resources: Define nouns (e.g., users, payments), use HTTP verbs, and plan nested resources or query parameters.
  3. Design Endpoints: Specify URIs, HTTP methods, request/response formats using Java 21+ Records.
  4. Implement Features: Add idempotency keys, pagination, rate limiting with virtual threads for concurrency.
  5. Analyze Complexity: Provide time and space complexity for key operations, e.g., O(1) for cache lookups.
  6. Handle Errors: Define HTTP status codes and error messages for various scenarios.
  7. Test Edge Cases: Include null inputs, duplicate requests, and high load simulations.
  8. State Trade-offs: Compare design choices like versioning methods or REST vs GraphQL.
  9. Summarize: Recap the design, emphasizing Java 21+ features and scalability considerations.

This template provides a structured approach for solving API design problems in interviews, integrating the steps covered in this section.

Verification Exercise: Design a Payment API in 10 Minutes

Apply the concepts by designing a payment processing API with idempotency keys, retry logic, and proper error codes within 10 minutes. Start by clarifying requirements: the API should handle payments with amounts and currencies, ensure no duplicate processing via idempotency keys, and return appropriate HTTP status codes (e.g., 201 for successful creation, 400 for invalid input, 429 for rate limiting). Model resources as payments, with endpoints like POST /payments for creation. Use Java 21+ Records for request and response models, implement idempotency caching as in the code example, and include rate limiting based on user quotas. Analyze complexity: O(1) for idempotency key lookups and O(n) for concurrent processing with virtual threads. Handle edge cases such as network timeouts by implementing retry logic with exponential backoff. State trade-offs, such as using URL versioning for clarity versus header versioning for decoupling. Summarize the design, ensuring it meets scalability needs by referencing capacity estimation from CH6-S1 for QPS calculations.

By following this exercise, you reinforce the ability to craft RESTful APIs that are robust, scalable, and interview-ready, leveraging Java 21+ features for modern system design.