TO CHECK
In distributed systems and RESTful APIs, UUIDs and Idempotency are critical patterns used to ensure data integrity, prevent duplicate resource creation, and gracefully handle network failures.
1. Resource ID vs. Idempotency Key (The Two Roles of UUIDs)
When building an idempotent REST API, UUIDs (Universally Unique Identifiers) are used in two completely distinct layers:
[vanilla] 1. The Resource ID (Data Identity) Represents a permanent object in your database.
- Who generates it: Can be generated by the client or server.
- Where it lives: In the URL path or response body.
- Example:
GET /api/v1/orders/8f5da7d2-43fa-4e76-88a9-4673c68fb761
[vanilla] 2. The Idempotency Key (Operation Identity) Represents a single, specific execution intent (e.g., a specific button click).
- Who generates it: Always generated by the client before making the request.
- Where it lives: Transmitted via a custom HTTP Header (
Idempotency-Key).- Example:
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
2. HTTP Method Contracts (Where Idempotency Fits)
An API operation is idempotent if making the same request multiple times yields the same server state as making it exactly once.
The REST Method Matrix
HTTP Method Idempotent? Safe? Description GETYes Yes Read-only. Fetching a resource 100 times changes nothing. PUTYes No Replaces a resource entirely. Repeated calls yield the exact same final state. DELETEYes No The first call deletes the resource. Subsequent calls return 404 Not Found, but the data remains gone.POSTNo No Natively non-idempotent. Sending POST /ordersthree times creates three separate charges.
3. The Intersection: Using an Operation UUID to fix POST
Because POST is not naturally idempotent, network drops create a major risk: if the client retries a timed-out POST request, they might cause double purchases.
By requiring the client to pass a unique UUID as an Idempotency Key, the server can track and safely handle retries of the same HTTP operation.
The Workflow Diagram
sequenceDiagram autonumber actor Client participant API Gateway / Middleware participant Database / Cache Note over Client: Step 1: Client generates a random UUID<br/>for this specific execution attempt.<br/>Operation UUID = 9b1deb4d... Client->>API Gateway: POST /orders (Header -> Idempotency-Key: 9b1deb4d...) API Gateway->>Database / Cache: Check if Operation UUID (9b1deb4d...) exists / Try Lock alt Scenario A: Operation UUID Not Found (First Attempt) Database / Cache-->>API Gateway: Key empty -> Acquire lock Note over API Gateway: Server processes order logic &<br/>creates a completely new Resource UUID (e.g. order_777) API Gateway->>Database / Cache: Store Operation UUID + Payload Hash + Response Data API Gateway-->>Client: 201 Created [Resource: order_777] (Network drops here!) else Scenario B: Operation UUID Found & Active (Concurrent Race Condition) Database / Cache-->>API Gateway: State is 'PROCESSING' API Gateway-->>Client: 409 Conflict (Client sent identical operation UUID while first is running) else Scenario C: Operation UUID Found But Payload Changed (Tampering / Bug) Database / Cache-->>API Gateway: Key exists but Payload Hash mismatches API Gateway-->>Client: 400 Bad Request (Cannot change request body on a reused key) else Scenario D: Operation UUID Found & Completed (Subsequent Safe Retry) Database / Cache-->>API Gateway: Key exists & Hash matches -> Return saved response API Gateway-->>Client: 201 Created [Resource: order_777] (Instantly returned from cache, no duplicate order created) end
Cache Best Practice
Set a Time-To-Live (TTL) on the Operation UUID key in your cache layer (e.g., Redis) for 24 to 48 hours. This clears out old transaction tokens while safely protecting against network-retry windows.
4. Advanced Scenario: Payload Mismatch Protection
What if a broken or malicious client sends the same Operation UUID key, but alters the underlying request body (e.g., changes the transaction amount from $10 to $1,000)?
Your application must prevent this. To implement payload verification:
- When the first request arrives, calculate a SHA-256 hash of the raw HTTP request body string.
- Store both the Status/Response and the Payload Hash together inside the cache metadata under the Operation UUID key.
- Upon retry, recalculate the body hash. If the incoming hash does not match the cached hash, immediately reject the execution with an
HTTP 400 Bad Request.
5. Crash Recovery Strategy (Handling 5xx System Failures)
What happens if the downstream database or payment gateway crashes while your key is set to "PROCESSING"?
The Deadlock Risk
If an unexpected runtime exception kills the request lifecycle, the Operation UUID could remain stuck in the cache as
"PROCESSING"for the duration of its lock TTL (e.g., 2 minutes). Any immediate automatic retry by the client would be incorrectly rejected with a409 Conflict.
Solution: Structural Cache Eviction
Your idempotency system must wrap execution inside a structured try-catch block. If the core business logic fails with an unhandled exception or a 5xx Internal Server Error, the middleware must evict (DELETE) the Operation UUID from the cache in the catch/finally block. This allows the client to retry the operation cleanly when downstream services stabilize.
6. Frontend Integration: CORS Headers
When your backend uses a custom header like Idempotency-Key, web browsers block it by default due to Cross-Origin Resource Sharing (CORS) rules.
To allow clients to successfully use this pattern, your API server must explicitly allow and expose the header.
CORS Headers Configuration
Access-Control-Allow-Headers: Must explicitly includeIdempotency-Keyso the browser is permitted to send it duringPOSToperations.Access-Control-Expose-Headers: If your server returns custom idempotency verification status tracking metrics in headers, add them here so frontend JavaScript can read them.
7. Distributed Tracing (Observability with OpenTelemetry)
When debugging failures across multiple microservices, you need to know if a slow request trace was the original attempt or a cached retry.
OpenTelemetry Best Practices
Inside your idempotency middleware/interceptor, inject custom attributes into the current trace Span:
- Set
idempotency.key = <UUID>so you can search all telemetry data by that specific operation ID.- Set
idempotency.is_retry = truewhen short-circuiting the request via the cache. This helps you identify network recovery spikes in monitoring dashboards (like Jaeger or Datadog).
8. Frontend JavaScript/TypeScript Implementation
When firing network operations, frontend apps shouldn’t blindly generate new UUIDs on every retry attempt. Instead, the Operation UUID must be bound to the lifecycle of the user’s action (e.g., generating the key when the form component renders or right when the submit click fires).
Below is a production-grade TypeScript implementation using Axios Interceptors to automatically pass and retain the same Idempotency-Key through network drop retries.
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { v4 as uuidv4 } from 'uuid';
const apiClient: AxiosInstance = axios.create({
baseURL: 'https://example.com',
timeout: 5000,
});
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (config.method?.toLowerCase() === 'post') {
if (!config.headers['Idempotency-Key']) {
config.headers['Idempotency-Key'] = uuidv4();
}
}
return config;
}, (error) => Promise.reject(error));
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (originalRequest && !originalRequest._retry && (error.code === 'ECONNABORTED' || !error.response)) {
originalRequest._retry = true;
console.warn(`Transient network drop detected. Retrying request safely using matching key: ${originalRequest.headers['Idempotency-Key']}`);
await new Promise(resolve => setTimeout(resolve, 2000));
return apiClient(originalRequest);
}
if (error.response?.status === 409) {
console.warn("Operation is active on server. Retrying soon...");
await new Promise(resolve => setTimeout(resolve, 3000));
return apiClient(originalRequest);
}
return Promise.reject(error);
}
);
export default apiClient;9. Asynchronous Systems & Distributed Sagas
For heavy workflows (e.g., an order request that takes 20 seconds because it processes third-party fulfillment, fraud evaluations, and ledger balances), you cannot hold an HTTP connection open. You must return an immediate HTTP 202 Accepted response and hand the transaction to a background worker or an Asynchronous Distributed Saga (State Machine).
The Async Idempotency Challenge
In async systems, the HTTP REST endpoint is just a gateway. The operation identity (UUID) cannot simply sit in a temporary Redis memory cache; it must be stored inside a persistent state store (like an Idempotent Consumer Table or Inbox Pattern DB) to maintain consistency even if background workers crash.
Architectural Workflow Pattern
- The Ingest Phase: The REST layer validates the
Idempotency-Key(UUID). If it’s new, it creates a record in the databaseIdempotent_Submissionstable with a status ofSTARTEDand returns a202 Acceptedwith a status URL. - The Worker execution: A messaging mechanism (RabbitMQ/Kafka) carries the operation payload, keeping the Operation UUID bound to the event metadata.
- The Saga Execution: Every step in the microservice mesh checks this central/inbox context. If Step 2 (Inventory Allocation) crashes mid-run, the engine reads the state of that specific transaction token to figure out exactly where to resume or roll back.
- Completion: When the saga finishes, the final execution response payload is updated in the database against that original
Idempotency-Key. Any subsequent REST client polling orPOSTretries will read that permanent record instead of kicking off another heavy background worker process.
10. Backend Code Implementations
Option A: .NET Core Middleware Implementation
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public class IdempotencyMiddleware {
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;
private static readonly ActivitySource IdempotencyActivitySource = new("Custom.Idempotency.Middleware");
public IdempotencyMiddleware(RequestDelegate next, IDistributedCache cache) {
_next = next;
_cache = cache;
}
public async Task InvokeAsync(HttpContext context) {
if (context.Request.Method != HttpMethods.Post || !context.Request.Headers.TryGetValue("Idempotency-Key", out
var itemKey)) {
await _next(context);
return;
}
string operationUuid = itemKey.ToString();
var activity = IdempotencyActivitySource.StartActivity("ExecuteIdempotentPost");
activity ? .SetTag("idempotency.key", operationUuid);
context.Request.EnableBuffering();
using
var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true);
string requestBodyText = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
string currentPayloadHash = ComputeSha256Hash(requestBodyText);
var cachedResponse = await _cache.GetStringAsync(operationUuid);
if (cachedResponse == "PROCESSING") {
activity ? .SetTag("idempotency.status", "conflict_locked");
context.Response.StatusCode = StatusCodes.Status409Conflict;
await context.Response.WriteAsync("This identical operation is already being processed.");
return;
}
if (cachedResponse != null) {
var savedResponse = JsonSerializer.Deserialize(cachedResponse);
if (savedResponse.PayloadHash != currentPayloadHash) {
activity ? .SetTag("idempotency.status", "payload_mismatch");
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{"
error ": "
Payload mismatch."}");
return;
}
activity ? .SetTag("idempotency.is_retry", true);
activity ? .SetTag("idempotency.status", "cache_hit");
context.Response.StatusCode = savedResponse.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(savedResponse.Body);
return;
}
activity ? .SetTag("idempotency.is_retry", false);
await _cache.SetStringAsync(operationUuid, "PROCESSING", new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
});
var originalResponseBodyStream = context.Response.Body;
using
var responseBody = new MemoryStream();
context.Response.Body = responseBody;
bool processingSuccessful = false;
try {
await _next(context);
processingSuccessful = true;
}
catch(Exception) {
await _cache.RemoveAsync(operationUuid);
throw;
}
finally {
if (processingSuccessful) {
context.Response.Body.Seek(0, SeekOrigin.Begin);
string responseText = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
if (context.Response.StatusCode >= 500) {
await _cache.RemoveAsync(operationUuid);
}
else {
var resultToCache = new CachedResponseModel {
StatusCode = context.Response.StatusCode,
Body = responseText,
PayloadHash = currentPayloadHash
};
await _cache.SetStringAsync(operationUuid, JsonSerializer.Serialize(resultToCache), new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)
});
}
}
await responseBody.CopyToAsync(originalResponseBodyStream);
activity ? .Dispose();
}
}
private static string ComputeSha256Hash(string rawData) {
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawData));
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++) builder.Append(bytes[i].ToString("x2"));
return builder.ToString();
}
}
public class CachedResponseModel {
public int StatusCode {
get;
set;
}
public string Body {
get;
set;
}
public string PayloadHash {
get;
set;
}
}Option B: Java (Spring Boot) Implementation
Step 1: Content Caching & CORS Configuration
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collections;
@Configuration
public class WebInfrastructureConfig {
@Bean
public FilterRegistrationBean cachingFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean < > ();
registrationBean.setFilter((req, res, chain) - > {
if (req instanceof HttpServletRequest) {
chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest) req), res);
} else {
chain.doFilter(req, res);
}
});
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
return registrationBean;
}
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Collections.singletonList("*"));
config.addAllowedHeader("Idempotency-Key");
config.addAllowedHeader("Content-Type");
config.addAllowedMethod("POST");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean < > (new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}Step 2: Interceptor Layer
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.opentelemetry.api.trace.Span;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
@Component
public class IdempotencyInterceptor implements HandlerInterceptor {
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
public IdempotencyInterceptor(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!"POST".equalsIgnoreCase(request.getMethod())) {
return true;
}
String operationUuid = request.getHeader("Idempotency-Key");
if (operationUuid == null || operationUuid.isEmpty()) {
return true;
}
Span currentSpan = Span.current();
currentSpan.setAttribute("idempotency.key", operationUuid);
ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request;
String rawBody = new String(wrappedRequest.getContentAsByteArray(), StandardCharsets.UTF_8);
String incomingHash = computeSha256(rawBody);
Boolean isNewOperation = redisTemplate.opsForValue().setIfAbsent(operationUuid, "PROCESSING", 2, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(isNewOperation)) {
String currentStatus = redisTemplate.opsForValue().get(operationUuid);
if ("PROCESSING".equals(currentStatus)) {
currentSpan.setAttribute("idempotency.status", "lock_conflict");
response.setStatus(409);
response.getWriter().write("{"
error ": "
Concurrent operation execution in progress.
"}");
return false;
}
CachedResponseData cachedData = objectMapper.readValue(currentStatus, CachedResponseData.class);
if (!cachedData.getPayloadHash().equals(incomingHash)) {
currentSpan.setAttribute("idempotency.status", "payload_mismatch");
response.setStatus(400);
response.getWriter().write("{"
error ": "
Payload mismatch.
"}");
return false;
}
currentSpan.setAttribute("idempotency.is_retry", true);
currentSpan.setAttribute("idempotency.status", "cache_hit");
response.setStatus(cachedData.getStatusCode());
response.setContentType("application/json");
response.getWriter().write(cachedData.getBody());
return false;
}
currentSpan.setAttribute("idempotency.is_retry", false);
request.setAttribute("currentOperationUuid", operationUuid);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String operationUuid = (String) request.getAttribute("currentOperationUuid");
if (operationUuid == null) return;
if (ex != null || response.getStatus() >= 500) {
redisTemplate.delete(operationUuid);
}
}
private String computeSha256(String base) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(base.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b: hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}10. Relational DB Schema Configuration (Inbox / Outbox Saga Engine)
When orchestration logic drops off the transient web thread into long-running persistence, you must substitute standard distributed memory caches with strict atomic transaction indexes using schemas like the one below.
-- PostgreSQL Version
CREATE TABLE idempotent_inbox (
operation_uuid UUID PRIMARY KEY,
payload_hash VARCHAR(64) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING', -- PROCESSING, COMPLETED, FAILED
http_status_code INT NULL,
response_body TEXT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_idempotent_inbox_status ON idempotent_inbox(status);
-- Example of an Atomic Lock-Insert Verification transaction query
-- If this returns 0 rows affected, a concurrent operation is active or completed.
INSERT INTO idempotent_inbox (operation_uuid, payload_hash, status)
VALUES ('9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'PROCESSING')
ON CONFLICT (operation_uuid) DO NOTHING; 11. High-Load Concurrent Performance Testing Script
To verify that your atomic cache locks work correctly under heavy load and prevent race conditions (throwing 409 Conflict inside rapid execution bounds), use this performance testing script written for the k6 metrics engine.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
race_condition_test: {
executor: 'constant-vus',
vus: 10,
duration: '5s',
},
},
};
const staticOperationUuid = '7b2d91c1-4c12-4290-a29d-bb8361099e2b';
export default function () {
const url = 'example.com';
const payload = JSON.stringify({ amount: 50.00, item: 'Enterprise License' });
const params = {
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': staticOperationUuid,
},
};
const res = http.post(url, payload, params);
check(res, {
'Is expected execution block status code (200, 201, or 409)': (r) => [200, 201, 409].includes(r.status),
'Is not double processed leakage (Never returns 500)': (r) => r.status !== 500,
});
sleep(0.1);
} 12. Automated Unit & Integration Testing (Verifying Code Paths)
To guarantee long-term stability, write automated test assertions that simulate execution branches.
Option 1: .NET Core Middleware Testing with xUnit & Moq
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Moq;
using System.Text;
using System.Text.Json;
using Xunit;
public class IdempotencyMiddlewareTests
{
private readonly Mock _mockCache;
private readonly IdempotencyMiddleware _middleware;
private bool _nextCalled = false;
public IdempotencyMiddlewareTests()
{
_mockCache = new Mock();
RequestDelegate next = (ctx) => {
_nextCalled = true;
ctx.Response.StatusCode = StatusCodes.Status201Created;
return Task.CompletedTask;
};
_middleware = new IdempotencyMiddleware(next, _mockCache.Object);
}
[Fact]
public async Task InvokeAsync_RejectsModifiedPayload_Returns400BadRequest()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Method = "POST";
string testUuid = Guid.NewGuid().ToString();
context.Request.Headers["Idempotency-Key"] = testUuid;
// Setup payload string
string alteredBody = "{"amount":999}";
var bodyBytes = Encoding.UTF8.GetBytes(alteredBody);
context.Request.Body = new MemoryStream(bodyBytes);
// Mock existing cached response containing a different hash signature
var mockSavedMetadata = new CachedResponseModel {
StatusCode = 201,
Body = "{}",
PayloadHash = "completely_different_hash_value"
};
var cachedString = JsonSerializer.Serialize(mockSavedMetadata);
_mockCache.Setup(c => c.GetAsync(testUuid, default)).ReturnsAsync(Encoding.UTF8.GetBytes(cachedString));
// Act
await _middleware.InvokeAsync(context);
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
Assert.False(_nextCalled); // Middleware should short-circuit before execution pipeline
}
} Option 2: Java Spring Interceptor Testing with JUnit 5 & Mockito
import static org.mockito.Mockito._;
import static org.junit.jupiter.api.Assertions._;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.util.ContentCachingRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
public class IdempotencyInterceptorTest {
private IdempotencyInterceptor interceptor;
@Mock private StringRedisTemplate redisTemplate;
@Mock private ValueOperations<String, String> valueOperations;
@Mock private ContentCachingRequestWrapper request;
@Mock private HttpServletResponse response;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
interceptor = new IdempotencyInterceptor(redisTemplate);
}
@Test
public void preHandle_ConcurrentRequests_Returns409Conflict() throws Exception {
// Arrange
String testUuid = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d";
when(request.getMethod()).thenReturn("POST");
when(request.getHeader("Idempotency-Key")).thenReturn(testUuid);
when(request.getContentAsByteArray()).thenReturn("{}".getBytes(StandardCharsets.UTF_8));
// Simulate lock attempt failure (SETNX returns false because key is active)
when(valueOperations.setIfAbsent(eq(testUuid), eq("PROCESSING"), anyLong(), any())).thenReturn(false);
when(valueOperations.get(testUuid)).thenReturn("PROCESSING");
StringWriter stringWriter = new StringWriter();
when(response.getWriter()).thenReturn(new PrintWriter(stringWriter));
// Act
boolean result = interceptor.preHandle(request, response, new Object());
// Assert
assertFalse(result); // Connection intercepts and breaks execution
verify(response).setStatus(409); // Correct conflict code asserted
}
}