// Core Types
org.springframework.boot.actuate.web.exchanges.HttpExchange
org.springframework.boot.actuate.web.exchanges.HttpExchange.Request
org.springframework.boot.actuate.web.exchanges.HttpExchange.Response
org.springframework.boot.actuate.web.exchanges.Include
// Repository
org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository
org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository
// Endpoint
org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint
// Recording Interfaces
org.springframework.boot.actuate.web.exchanges.RecordableHttpRequest
org.springframework.boot.actuate.web.exchanges.RecordableHttpResponseWhat HTTP data needs tracking?
│
├─ All traffic (headers, timing, principals)?
│ └─ Include.REQUEST_HEADERS, RESPONSE_HEADERS, PRINCIPAL,
│ TIME_TAKEN, REMOTE_ADDRESS
│
├─ Minimal tracking (performance monitoring)?
│ └─ Include.TIME_TAKEN, REMOTE_ADDRESS only
│
├─ Debug mode (everything including cookies)?
│ └─ All Include options
│
└─ Production (exclude sensitive data)?
└─ Exclude AUTHORIZATION_HEADER, COOKIE_HEADERS| Option | Data Captured | Sensitive? | Default? |
|---|---|---|---|
| REQUEST_HEADERS | Request headers (excluding cookies/auth) | Partial | Yes |
| RESPONSE_HEADERS | Response headers (excluding cookies) | No | Yes |
| COOKIE_HEADERS | Cookie headers | Yes | No |
| AUTHORIZATION_HEADER | Authorization header | Yes | No |
| PRINCIPAL | Authenticated user | No | No |
| SESSION_ID | Session identifier | Yes | No |
| TIME_TAKEN | Request duration | No | Yes |
| REMOTE_ADDRESS | Client IP address | Partial | No |
Always track:
Consider tracking:
Don't track:
PATTERN: Production-safe includes
// ✓ Safe for production
Set<Include> includes = Set.of(
Include.TIME_TAKEN,
Include.REMOTE_ADDRESS,
Include.REQUEST_HEADERS, // Safe (excludes auth)
Include.RESPONSE_HEADERS, // Safe (excludes cookies)
Include.PRINCIPAL // Non-sensitive
);ANTI-PATTERN: Include everything
// ❌ Security risk
Set<Include> includes = EnumSet.allOf(Include.class);
// Exposes Authorization headers, cookies, session IDs!PATTERN: Appropriate capacity
// ✓ Size based on traffic
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
InMemoryHttpExchangeRepository repository =
new InMemoryHttpExchangeRepository();
// 1000 req/min * 60 min = last hour
repository.setCapacity(60000);
return repository;
}ANTI-PATTERN: Unbounded storage
// ❌ Memory leak
@Bean
public HttpExchangeRepository httpExchangeRepository() {
// Using unlimited custom implementation
return new UnlimitedHttpExchangeRepository(); // Will grow forever!
}PATTERN: Analysis-focused queries
// ✓ Structured analysis
List<HttpExchange> slowRequests = repository.findAll().stream()
.filter(ex -> ex.getTimeTaken() != null)
.filter(ex -> ex.getTimeTaken().toMillis() > 1000)
.collect(Collectors.toList());The HTTP exchange tracking framework records HTTP request-response exchanges for monitoring web traffic, analyzing API usage, and debugging HTTP interactions. It provides configurable capture options, in-memory storage, and actuator endpoint integration.
/**
* Interface for HTTP requests that can be recorded by the exchange tracking framework.
* Implementations provide request metadata needed for exchange recording.
*
* Thread-safe: Implementations should be thread-safe
* Package: org.springframework.boot.actuate.web.exchanges
* @since 3.0.0
*/
public interface RecordableHttpRequest {
/**
* Return the HTTP method of the request.
*
* @return the HTTP method (never null)
*/
String getMethod();
/**
* Return the URI of the request.
*
* @return the URI (never null)
*/
URI getUri();
/**
* Return the headers of the request.
*
* @return map of header names to values (never null, may be empty)
*/
Map<String, List<String>> getHeaders();
/**
* Return the remote address (client IP) of the request, if available.
*
* @return the remote address (may be null)
*/
@Nullable String getRemoteAddress();
}
/**
* Interface for HTTP responses that can be recorded by the exchange tracking framework.
* Implementations provide response metadata needed for exchange recording.
*
* Thread-safe: Implementations should be thread-safe
* Package: org.springframework.boot.actuate.web.exchanges
* @since 3.0.0
*/
public interface RecordableHttpResponse {
/**
* Return the HTTP status code of the response.
*
* @return the status code
*/
int getStatus();
/**
* Return the headers of the response.
*
* @return map of header names to values (never null, may be empty)
*/
Map<String, List<String>> getHeaders();
}
/**
* HTTP request and response exchange.
*
* Thread-safe: Immutable after construction
* Nullability: Principal, session, timeTaken can be null
* Since: Spring Boot 3.0+ (replaces HttpTrace from 2.x)
*/
public final class HttpExchange {
/**
* Create HTTP exchange.
*
* @param timestamp Exchange timestamp (non-null)
* @param request Request details (non-null)
* @param response Response details (non-null)
* @param principal Authenticated principal (nullable)
* @param session Session details (nullable)
* @param timeTaken Request duration (nullable)
*/
public HttpExchange(Instant timestamp, Request request, Response response,
@Nullable Principal principal, @Nullable Session session, @Nullable Duration timeTaken);
/**
* Start recording an exchange.
*
* @param request Recordable request (non-null)
* @return Started exchange
*/
public static Started start(RecordableHttpRequest request);
/**
* Start recording with custom clock.
*
* @param clock Clock for timestamps
* @param request Recordable request
* @return Started exchange
*/
public static Started start(Clock clock, RecordableHttpRequest request);
public Instant getTimestamp();
public Request getRequest();
public Response getResponse();
public @Nullable Principal getPrincipal();
public @Nullable Session getSession();
public @Nullable Duration getTimeTaken();
/**
* Started exchange ready to be finished.
*/
public static final class Started {
/**
* Finish exchange with response using system UTC clock and varargs includes.
*
* @param response Recordable response
* @param principalSupplier Principal supplier
* @param sessionIdSupplier Session ID supplier
* @param includes What to include
* @return Completed exchange
*/
public HttpExchange finish(RecordableHttpResponse response,
Supplier<@Nullable Principal> principalSupplier,
Supplier<@Nullable String> sessionIdSupplier,
Include... includes);
/**
* Finish exchange with response using custom clock and varargs includes.
*
* @param clock Clock for timestamp
* @param response Recordable response
* @param principalSupplier Principal supplier
* @param sessionIdSupplier Session ID supplier
* @param includes What to include
* @return Completed exchange
*/
public HttpExchange finish(Clock clock, RecordableHttpResponse response,
Supplier<@Nullable Principal> principalSupplier,
Supplier<@Nullable String> sessionIdSupplier,
Include... includes);
/**
* Finish exchange with response using system UTC clock and Set of includes.
*
* @param response Recordable response
* @param principalSupplier Principal supplier
* @param sessionIdSupplier Session ID supplier
* @param includes What to include as a Set
* @return Completed exchange
*/
public HttpExchange finish(RecordableHttpResponse response,
Supplier<@Nullable Principal> principalSupplier,
Supplier<@Nullable String> sessionIdSupplier,
Set<Include> includes);
/**
* Finish exchange with response using custom clock and Set of includes.
*
* @param clock Clock for timestamp
* @param response Recordable response
* @param principalSupplier Principal supplier
* @param sessionIdSupplier Session ID supplier
* @param includes What to include as a Set
* @return Completed exchange
*/
public HttpExchange finish(Clock clock, RecordableHttpResponse response,
Supplier<@Nullable Principal> principalSupplier,
Supplier<@Nullable String> sessionIdSupplier,
Set<Include> includes);
}
/**
* HTTP request details.
*/
public static final class Request {
public Request(URI uri, @Nullable String remoteAddress, String method,
Map<String, List<String>> headers);
public String getMethod();
public URI getUri();
public Map<String, List<String>> getHeaders();
public @Nullable String getRemoteAddress();
}
/**
* HTTP response details.
*/
public static final class Response {
public Response(int status, Map<String, List<String>> headers);
public int getStatus();
public Map<String, List<String>> getHeaders();
}
/**
* Principal details.
*/
public static final class Principal {
public Principal(String name);
public String getName();
}
/**
* Session details.
*/
public static final class Session {
public Session(String id);
public String getId();
}
}
/**
* Options for what to include in exchanges.
*
* Since: Spring Boot 3.0+
*/
public enum Include {
REQUEST_HEADERS,
RESPONSE_HEADERS,
COOKIE_HEADERS,
AUTHORIZATION_HEADER,
PRINCIPAL,
SESSION_ID,
TIME_TAKEN,
REMOTE_ADDRESS;
/**
* Default includes.
*
* @return TIME_TAKEN, REQUEST_HEADERS, RESPONSE_HEADERS
*/
public static Set<Include> defaultIncludes();
}Repository for storing and retrieving HTTP exchanges.
/**
* Repository for storing and retrieving HTTP exchanges.
*
* Thread-safe: Implementations should be thread-safe
* Package: org.springframework.boot.actuate.web.exchanges
* @since 3.0.0
*/
public interface HttpExchangeRepository {
/**
* Return all HTTP exchanges currently stored in the repository.
*
* @return list of exchanges (never null, may be empty)
*/
List<HttpExchange> findAll();
/**
* Add a new HTTP exchange to the repository.
*
* @param exchange the exchange to add (non-null)
*/
void add(HttpExchange exchange);
}
/**
* In-memory implementation of HttpExchangeRepository with circular buffer storage.
*
* Thread-safe: Yes (synchronized internally)
* Default capacity: 100 exchanges
* Package: org.springframework.boot.actuate.web.exchanges
* @since 3.0.0
*/
public class InMemoryHttpExchangeRepository implements HttpExchangeRepository {
/**
* Create repository with default capacity (100).
*/
public InMemoryHttpExchangeRepository();
/**
* Set maximum number of exchanges to store (circular buffer).
* Older exchanges are automatically removed when capacity is exceeded.
*
* @param capacity maximum exchanges (must be positive)
*/
public void setCapacity(int capacity);
/**
* Get current capacity setting.
*
* @return maximum number of exchanges
*/
public int getCapacity();
/**
* Set whether exchanges should be listed in reverse order (newest first).
* By default, exchanges are stored in reverse order.
*
* @param reverse true for reverse order (newest first), false for natural order (oldest first)
*/
public void setReverse(boolean reverse);
@Override
public List<HttpExchange> findAll();
@Override
public void add(HttpExchange exchange);
}Actuator endpoint for exposing HTTP exchange information.
/**
* Endpoint to expose HTTP exchange information via the actuator framework.
*
* Thread-safe: Yes
* Endpoint ID: "httpexchanges"
* Package: org.springframework.boot.actuate.web.exchanges
* @since 3.0.0
*/
@Endpoint(id = "httpexchanges")
public class HttpExchangesEndpoint {
/**
* Create endpoint with exchange repository.
*
* @param repository exchange repository (non-null)
* @throws IllegalArgumentException if repository is null
*/
public HttpExchangesEndpoint(HttpExchangeRepository repository);
/**
* Get all recorded HTTP exchanges.
* Exposed as a read operation via the actuator endpoint.
*
* @return descriptor containing all exchanges (never null)
*/
@ReadOperation
public HttpExchangesDescriptor httpExchanges();
/**
* Response descriptor for HTTP exchanges endpoint.
* Implements OperationResponseBody for proper actuator serialization.
*/
public static final class HttpExchangesDescriptor implements OperationResponseBody {
/**
* Get the list of HTTP exchanges.
*
* @return list of exchanges (never null)
*/
public List<HttpExchange> getExchanges();
}
}package com.example.actuator;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration for HTTP exchange tracking.
*
* Thread-safe: Yes
* Auto-recording: Yes (via auto-configuration)
* Since: Application 1.0
*/
@Configuration
public class HttpExchangeConfiguration {
/**
* In-memory HTTP exchange repository.
*
* @return Repository storing last 1000 exchanges
*/
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
InMemoryHttpExchangeRepository repository =
new InMemoryHttpExchangeRepository();
// Store last 1000 exchanges
repository.setCapacity(1000);
// Newest first (default)
repository.setReverse(false);
return repository;
}
/**
* HTTP exchanges endpoint.
*
* @param repository Exchange repository
* @return Configured endpoint
*/
@Bean
public HttpExchangesEndpoint httpExchangesEndpoint(
HttpExchangeRepository repository) {
return new HttpExchangesEndpoint(repository);
}
}
// Configuration in application.yml
/*
management:
endpoints:
web:
exposure:
include: httpexchanges
httpexchanges:
recording:
enabled: true
include:
- request-headers
- response-headers
- principal
- time-taken
- remote-address
*/package com.example.actuator;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service for analyzing HTTP traffic patterns.
*
* Thread-safe: Yes
* Performance: O(n) for most operations
* Since: Application 1.0
*/
@Service
public class HttpAnalyticsService {
private final HttpExchangeRepository repository;
public HttpAnalyticsService(HttpExchangeRepository repository) {
this.repository = repository;
}
/**
* Get average response time across all requests.
*
* @return Average duration in milliseconds
*/
public double getAverageResponseTime() {
return repository.findAll().stream()
.map(HttpExchange::getTimeTaken)
.filter(Objects::nonNull)
.mapToLong(Duration::toMillis)
.average()
.orElse(0.0);
}
/**
* Get request count by endpoint.
*
* @return Map of endpoint paths to request counts
*/
public Map<String, Long> getEndpointCallCounts() {
return repository.findAll().stream()
.collect(Collectors.groupingBy(
ex -> ex.getRequest().getUri().getPath(),
Collectors.counting()
));
}
/**
* Find slow requests (> threshold).
*
* @param thresholdMs Threshold in milliseconds
* @return List of slow requests
*/
public List<SlowRequest> findSlowRequests(long thresholdMs) {
return repository.findAll().stream()
.filter(ex -> ex.getTimeTaken() != null)
.filter(ex -> ex.getTimeTaken().toMillis() > thresholdMs)
.map(ex -> new SlowRequest(
ex.getRequest().getUri().toString(),
ex.getTimeTaken().toMillis(),
ex.getResponse().getStatus(),
ex.getRequest().getRemoteAddress()
))
.sorted(Comparator.comparingLong(SlowRequest::durationMs).reversed())
.collect(Collectors.toList());
}
/**
* Get status code distribution.
*
* @return Map of status codes to counts
*/
public Map<Integer, Long> getStatusCodeDistribution() {
return repository.findAll().stream()
.collect(Collectors.groupingBy(
ex -> ex.getResponse().getStatus(),
Collectors.counting()
));
}
/**
* Get error rate (4xx and 5xx responses).
*
* @return Error rate as percentage (0-100)
*/
public double getErrorRate() {
List<HttpExchange> exchanges = repository.findAll();
if (exchanges.isEmpty()) {
return 0.0;
}
long errors = exchanges.stream()
.map(ex -> ex.getResponse().getStatus())
.filter(status -> status >= 400)
.count();
return (errors * 100.0) / exchanges.size();
}
/**
* Get requests by client IP address.
*
* @return Map of IP addresses to request counts
*/
public Map<String, Long> getRequestsByIpAddress() {
return repository.findAll().stream()
.map(ex -> ex.getRequest().getRemoteAddress())
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(
ip -> ip,
Collectors.counting()
));
}
/**
* Get top slowest endpoints.
*
* @param limit Number of results
* @return List of endpoints with average response times
*/
public List<EndpointStats> getTopSlowestEndpoints(int limit) {
return repository.findAll().stream()
.filter(ex -> ex.getTimeTaken() != null)
.collect(Collectors.groupingBy(
ex -> ex.getRequest().getUri().getPath(),
Collectors.averagingLong(ex -> ex.getTimeTaken().toMillis())
))
.entrySet().stream()
.map(entry -> new EndpointStats(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparingDouble(EndpointStats::avgDurationMs).reversed())
.limit(limit)
.collect(Collectors.toList());
}
/**
* Detect potential brute force attacks.
*
* @param failureThreshold Number of failures to trigger alert
* @return List of suspicious IP addresses
*/
public List<SecurityAlert> detectBruteForceAttempts(int failureThreshold) {
Map<String, Long> failuresByIp = repository.findAll().stream()
.filter(ex -> ex.getResponse().getStatus() == 401 ||
ex.getResponse().getStatus() == 403)
.map(ex -> ex.getRequest().getRemoteAddress())
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(ip -> ip, Collectors.counting()));
return failuresByIp.entrySet().stream()
.filter(entry -> entry.getValue() >= failureThreshold)
.map(entry -> new SecurityAlert(
entry.getKey(),
entry.getValue(),
"Potential brute force attack"
))
.collect(Collectors.toList());
}
public record SlowRequest(String uri, long durationMs, int status, String remoteAddress) {}
public record EndpointStats(String endpoint, double avgDurationMs) {}
public record SecurityAlert(String ipAddress, long failureCount, String reason) {}
}package com.example.actuator;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Controller for HTTP performance dashboard.
*
* Thread-safe: Yes (via Spring MVC)
* Since: Application 1.0
*/
@Controller
@RequestMapping("/admin/http-monitoring")
public class HttpMonitoringDashboard {
private final HttpAnalyticsService analyticsService;
public HttpMonitoringDashboard(HttpAnalyticsService analyticsService) {
this.analyticsService = analyticsService;
}
/**
* Display HTTP monitoring dashboard.
*
* @param model Spring MVC model
* @return View name
*/
@GetMapping
public String dashboard(Model model) {
// Overall metrics
model.addAttribute("avgResponseTime", analyticsService.getAverageResponseTime());
model.addAttribute("errorRate", analyticsService.getErrorRate());
// Endpoint statistics
model.addAttribute("endpointCounts", analyticsService.getEndpointCallCounts());
model.addAttribute("slowestEndpoints", analyticsService.getTopSlowestEndpoints(10));
// Status codes
model.addAttribute("statusDistribution", analyticsService.getStatusCodeDistribution());
// Security
model.addAttribute("securityAlerts", analyticsService.detectBruteForceAttempts(10));
model.addAttribute("requestsByIp", analyticsService.getRequestsByIpAddress());
// Slow requests
model.addAttribute("slowRequests", analyticsService.findSlowRequests(1000));
return "http-monitoring-dashboard";
}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class HttpAnalyticsServiceTest {
private InMemoryHttpExchangeRepository repository;
private HttpAnalyticsService service;
@BeforeEach
void setUp() {
repository = new InMemoryHttpExchangeRepository();
service = new HttpAnalyticsService(repository);
// Add test data
addExchange("/api/users", 200, Duration.ofMillis(100), "192.168.1.1");
addExchange("/api/users", 200, Duration.ofMillis(150), "192.168.1.2");
addExchange("/api/orders", 200, Duration.ofMillis(2000), "192.168.1.1"); // Slow
addExchange("/api/orders", 500, Duration.ofMillis(50), "192.168.1.3"); // Error
addExchange("/api/login", 401, Duration.ofMillis(30), "192.168.1.4"); // Auth failure
}
@Test
void getAverageResponseTime_CalculatesCorrectly() {
double avg = service.getAverageResponseTime();
assertThat(avg).isEqualTo(466.0); // (100+150+2000+50+30)/5
}
@Test
void getEndpointCallCounts_CountsCorrectly() {
Map<String, Long> counts = service.getEndpointCallCounts();
assertThat(counts).containsEntry("/api/users", 2L);
assertThat(counts).containsEntry("/api/orders", 2L);
assertThat(counts).containsEntry("/api/login", 1L);
}
@Test
void findSlowRequests_FindsRequestsAboveThreshold() {
List<HttpAnalyticsService.SlowRequest> slow = service.findSlowRequests(1000);
assertThat(slow).hasSize(1);
assertThat(slow.get(0).uri()).contains("/api/orders");
assertThat(slow.get(0).durationMs()).isEqualTo(2000);
}
@Test
void getStatusCodeDistribution_CountsByStatus() {
Map<Integer, Long> distribution = service.getStatusCodeDistribution();
assertThat(distribution).containsEntry(200, 3L);
assertThat(distribution).containsEntry(500, 1L);
assertThat(distribution).containsEntry(401, 1L);
}
@Test
void getErrorRate_CalculatesCorrectly() {
double errorRate = service.getErrorRate();
assertThat(errorRate).isEqualTo(40.0); // 2 errors out of 5 = 40%
}
@Test
void detectBruteForceAttempts_DetectsSuspiciousActivity() {
// Add more auth failures from same IP
for (int i = 0; i < 15; i++) {
addExchange("/api/login", 401, Duration.ofMillis(30), "192.168.1.100");
}
List<HttpAnalyticsService.SecurityAlert> alerts =
service.detectBruteForceAttempts(10);
assertThat(alerts).hasSize(1);
assertThat(alerts.get(0).ipAddress()).isEqualTo("192.168.1.100");
assertThat(alerts.get(0).failureCount()).isEqualTo(15);
}
private void addExchange(String path, int status, Duration timeTaken, String remoteAddress) {
HttpExchange.Request request = new HttpExchange.Request(
URI.create("http://localhost:8080" + path),
remoteAddress,
"GET",
Map.of()
);
HttpExchange.Response response = new HttpExchange.Response(status, Map.of());
HttpExchange exchange = new HttpExchange(
Instant.now(),
request,
response,
null,
null,
timeTaken
);
repository.add(exchange);
}
}Problem: GET /actuator/httpexchanges returns empty
Causes:
Solutions:
# Solution 1: Enable recording
management:
httpexchanges:
recording:
enabled: true
# Solution 2: Configure repository
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
# Solution 3: Check filter is registered (auto-configured by default)Problem: Application uses too much memory
Causes:
Solutions:
// Solution 1: Reduce capacity
repository.setCapacity(100); // Lower for high-traffic apps
// Solution 2: Exclude large headers
management:
httpexchanges:
recording:
include:
- time-taken
- remote-address
# Exclude headers for memory savings
// Solution 3: Use persistent repository
@Bean
public HttpExchangeRepository persistentRepository(DataSource ds) {
return new JdbcHttpExchangeRepository(ds);
}Problem: Authorization tokens visible in exchanges
Causes:
Solutions:
# Exclude sensitive headers
management:
httpexchanges:
recording:
include:
- request-headers # Safe (auto-excludes auth)
- response-headers # Safe (auto-excludes cookies)
- time-taken
# Don't include:
# - authorization-header
# - cookie-headers// Calculate based on traffic patterns
// Example: 1000 req/min, want 1 hour history
// Capacity = 1000 * 60 = 60,000
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
int requestsPerMinute = 1000;
int historyMinutes = 60;
int capacity = requestsPerMinute * historyMinutes;
InMemoryHttpExchangeRepository repository =
new InMemoryHttpExchangeRepository();
repository.setCapacity(capacity);
return repository;
}// Recording adds ~1-2ms overhead per request
// Minimize included data for high-traffic endpoints
// ✓ Minimal overhead
Set<Include> minimalIncludes = Set.of(
Include.TIME_TAKEN,
Include.REMOTE_ADDRESS
);
// ❌ Higher overhead
Set<Include> maximalIncludes = EnumSet.allOf(Include.class);// findAll() loads all exchanges into memory
// For large repositories, this can be expensive
// ✓ Good - filter in stream
List<HttpExchange> recent = repository.findAll().stream()
.filter(ex -> ex.getTimestamp().isAfter(cutoff))
.limit(100)
.collect(Collectors.toList());
// For very large datasets, implement custom repository with filtering