Skip to main content

๐Ÿ”„ Idempotency Pattern

๐Ÿ“‹ Overview and Problem Statementโ€‹

Definitionโ€‹

Idempotency ensures that multiple identical requests have the same effect as a single request, making operations safe to retry without unintended side effects.

Problems It Solvesโ€‹

  • Duplicate requests
  • Network failures
  • Retry scenarios
  • Double-processing
  • Transaction consistency
  • Message delivery guarantees

Business Valueโ€‹

  • Data consistency
  • System reliability
  • Safe retries
  • Reduced errors
  • Better user experience
  • Transaction safety

๐Ÿ—๏ธ Architecture & Core Conceptsโ€‹

Idempotency Flowโ€‹

System Componentsโ€‹

๐Ÿ’ป Technical Implementationโ€‹

Basic Idempotency Handlerโ€‹

@Service
public class IdempotencyHandler {
private final Cache<String, ResponseRecord> responseCache;
private final IdempotencyKeyGenerator keyGenerator;

public <T> T execute(
String idempotencyKey,
Supplier<T> operation
) {
// Check for existing response
ResponseRecord<T> existing = responseCache.get(
idempotencyKey);
if (existing != null) {
return existing.getResponse();
}

// Execute operation with locking
synchronized (idempotencyKey.intern()) {
// Double-check after acquiring lock
existing = responseCache.get(idempotencyKey);
if (existing != null) {
return existing.getResponse();
}

T result = operation.get();
responseCache.put(
idempotencyKey,
new ResponseRecord<>(result)
);
return result;
}
}
}

Distributed Idempotency Implementationโ€‹

@Service
public class DistributedIdempotencyService {
private final RedisTemplate<String, String> redis;
private final ObjectMapper objectMapper;
private final Duration lockTimeout;

public <T> T executeIdempotently(
String key,
Supplier<T> operation,
Class<T> responseType
) throws Exception {
String lockKey = "lock:" + key;
String resultKey = "result:" + key;

try {
// Try to acquire distributed lock
boolean locked = redis.opsForValue()
.setIfAbsent(lockKey, "locked", lockTimeout);

if (!locked) {
// Check for existing result
String cachedResult = redis.opsForValue()
.get(resultKey);
if (cachedResult != null) {
return objectMapper.readValue(
cachedResult,
responseType
);
}
throw new ConcurrentExecutionException();
}

// Execute operation
T result = operation.get();

// Store result
redis.opsForValue().set(
resultKey,
objectMapper.writeValueAsString(result),
lockTimeout
);

return result;
} finally {
redis.delete(lockKey);
}
}
}

Message Processing Idempotencyโ€‹

@Service
public class IdempotentMessageProcessor {
private final ProcessedMessageRepository repository;
private final MessageProcessor processor;

@Transactional
public void processMessage(Message message) {
String messageId = message.getMessageId();

// Check if already processed
if (repository.exists(messageId)) {
log.info("Message {} already processed", messageId);
return;
}

try {
// Process message
processor.process(message);

// Mark as processed
repository.save(new ProcessedMessage(
messageId,
ProcessingStatus.COMPLETED,
LocalDateTime.now()
));
} catch (Exception e) {
repository.save(new ProcessedMessage(
messageId,
ProcessingStatus.FAILED,
LocalDateTime.now()
));
throw e;
}
}
}

๐Ÿค” Decision Criteria & Evaluationโ€‹

Idempotency Strategy Comparisonโ€‹

StrategyUse CaseProsCons
Token-basedAPI requestsSimple to implementToken management overhead
Natural KeysBusiness operationsNo additional IDsMay not always be available
Timestamp-basedTime-series dataNatural orderingClock sync issues
UUID-basedDistributed systemsNo coordination neededStorage overhead

Implementation Comparison Matrixโ€‹

AspectIn-MemoryDistributed CacheDatabase
PerformanceFastestFastSlower
DurabilityNoneConfigurableHigh
ScalabilityLimitedGoodExcellent
ComplexityLowMediumHigh

โš ๏ธ Anti-Patternsโ€‹

1. Incorrect Key Generationโ€‹

โŒ Wrong:

public class WeakIdempotencyKey {
public String generateKey(Request request) {
// Using timestamp alone is not idempotent
return String.valueOf(System.currentTimeMillis());
}
}

โœ… Correct:

public class StrongIdempotencyKey {
public String generateKey(Request request) {
return String.format("%s:%s:%s",
request.getOperationType(),
request.getResourceId(),
request.getClientId()
);
}
}

2. Missing Cleanupโ€‹

โŒ Wrong:

public class LeakyIdempotencyStore {
private final Map<String, Response> responses =
new HashMap<>();

public void store(String key, Response response) {
responses.put(key, response); // Never cleaned up
}
}

โœ… Correct:

public class ManagedIdempotencyStore {
private final Cache<String, Response> responses;

public ManagedIdempotencyStore() {
this.responses = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofHours(24))
.maximumSize(10_000)
.build();
}

public void store(String key, Response response) {
responses.put(key, response);
}
}

๐Ÿ’ก Best Practicesโ€‹

1. Key Generationโ€‹

public class IdempotencyKeyGenerator {
public String generateKey(Request request) {
// Combine multiple factors for uniqueness
String payload = String.format("%s:%s:%s:%s",
request.getClientId(),
request.getOperationType(),
request.getResourceId(),
request.getTimestamp()
);

// Create cryptographic hash
return DigestUtils.sha256Hex(payload);
}
}

2. Response Cachingโ€‹

@Service
public class IdempotencyResponseCache {
private final Cache<String, CachedResponse> cache;

@Value
public class CachedResponse {
String responseBody;
HttpStatus status;
Map<String, String> headers;
LocalDateTime timestamp;
}

public void cacheResponse(
String key,
ResponseEntity<?> response
) {
CachedResponse cached = new CachedResponse(
serializeBody(response.getBody()),
response.getStatusCode(),
extractHeaders(response.getHeaders()),
LocalDateTime.now()
);
cache.put(key, cached);
}
}

๐Ÿ” Troubleshooting Guideโ€‹

Common Issuesโ€‹

  1. Race Conditions
public class RaceConditionDetector {
private final MetricRegistry metrics;

public void detectRaceConditions(String key) {
long concurrentAttempts = metrics
.counter("idempotency.concurrent.attempts")
.getCount();

if (concurrentAttempts > threshold) {
log.warn("Potential race condition detected for key: {}",
key);
alertService.sendAlert(
"High concurrent attempts for idempotency key: " +
key);
}
}
}
  1. Key Collisions
public class CollisionDetector {
public void detectCollisions(String key) {
List<String> similarKeys = findSimilarKeys(key);
if (!similarKeys.isEmpty()) {
log.warn("Potential key collision detected: {}",
similarKeys);
metrics.counter("idempotency.collisions").inc();
}
}
}

๐Ÿงช Testingโ€‹

Idempotency Testsโ€‹

@Test
public void testIdempotentExecution() {
// Arrange
IdempotencyHandler handler = new IdempotencyHandler();
AtomicInteger counter = new AtomicInteger();
String key = "test-key";

// Act
IntStream.range(0, 10).parallel().forEach(i -> {
handler.execute(key, () -> {
return counter.incrementAndGet();
});
});

// Assert
assertEquals(1, counter.get());
}

@Test
public void testConcurrentRequests() {
// Arrange
IdempotencyHandler handler = new IdempotencyHandler();
int threadCount = 100;
CountDownLatch latch = new CountDownLatch(threadCount);
Set<String> results = ConcurrentHashMap.newKeySet();

// Act
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
String result = handler.execute("key",
() -> UUID.randomUUID().toString());
results.add(result);
latch.countDown();
}).start();
}

latch.await();

// Assert
assertEquals(1, results.size());
}

๐ŸŒ Real-world Use Casesโ€‹

1. Payment Processingโ€‹

  • Transaction deduplication
  • Payment confirmations
  • Refund processing

2. Order Managementโ€‹

  • Order submissions
  • Status updates
  • Inventory adjustments

3. Message Processingโ€‹

  • Event handling
  • Queue processing
  • Webhook delivery

๐Ÿ“š Referencesโ€‹

Booksโ€‹

  • "Designing Data-Intensive Applications" by Martin Kleppmann
  • "Building Event-Driven Microservices" by Adam Bellemare

Online Resourcesโ€‹