Skip to content

Implementar Idempotência com chaves persistidas #20

@wallanpsantos

Description

@wallanpsantos

Título: Add idempotency support with persisted keys
Labels: critical, idempotency, reliability
Prioridade: CRÍTICA

Problema Atual (Ponto 4 do Feedback):

  • Ausência de checagens de idempotência
  • Risco de duplicidade em retries

Solução Proposta:

-- Tabela de idempotência
CREATE TABLE idempotency_keys (
    key VARCHAR(255) PRIMARY KEY,
    operation_type VARCHAR(50) NOT NULL,
    request_hash VARCHAR(255) NOT NULL,
    response_data JSONB,
    status VARCHAR(20) NOT NULL, -- PROCESSING, COMPLETED, FAILED
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,
    
    INDEX idx_expires_at (expires_at)
);
// DataProvider - Idempotency Service
@Service
@Transactional
public class IdempotencyService {
    
    @Transactional
    public <T> T executeIdempotent(String idempotencyKey, 
                                   String operationType,
                                   String requestHash,
                                   Supplier<T> operation,
                                   Class<T> responseType) {
        
        // 1. Verificar se já existe
        Optional<IdempotencyRecord> existing = 
            idempotencyRepository.findByKey(idempotencyKey);
            
        if (existing.isPresent()) {
            IdempotencyRecord record = existing.get();
            
            if (record.getStatus() == IdempotencyStatus.COMPLETED) {
                // Retornar resultado cached
                return deserializeResponse(record.getResponseData(), responseType);
            }
            
            if (record.getStatus() == IdempotencyStatus.PROCESSING) {
                throw new OperationInProgressException("Operation already in progress");
            }
        }
        
        // 2. Marcar como processando
        IdempotencyRecord record = IdempotencyRecord.builder()
            .key(idempotencyKey)
            .operationType(operationType)
            .requestHash(requestHash)
            .status(IdempotencyStatus.PROCESSING)
            .expiresAt(LocalDateTime.now().plus(TTL))
            .build();
            
        idempotencyRepository.save(record);
        
        try {
            // 3. Executar operação
            T result = operation.get();
            
            // 4. Marcar como concluído e salvar resultado
            record.setStatus(IdempotencyStatus.COMPLETED);
            record.setResponseData(serializeResponse(result));
            record.setCompletedAt(LocalDateTime.now());
            idempotencyRepository.save(record);
            
            return result;
            
        } catch (Exception e) {
            // 5. Marcar como falha
            record.setStatus(IdempotencyStatus.FAILED);
            idempotencyRepository.save(record);
            throw e;
        }
    }
}

// Uso nos Use Cases
public class DepositUseCase {
    
    public WalletTransaction execute(String userId, Money amount, String idempotencyKey) {
        String requestHash = calculateHash(userId, amount);
        
        return idempotencyService.executeIdempotent(
            idempotencyKey,
            "DEPOSIT",
            requestHash,
            () -> transactionalWalletService.executeDeposit(userId, amount),
            WalletTransaction.class
        );
    }
}

Tarefas:

  • Criar tabela idempotency_keys

  • Implementar IdempotencyService no dataprovider

  • Adicionar idempotencyKey em todos os requests

    public record DepositRequest(
        @NotNull BigDecimal amount,
        @NotBlank String currency,
        @NotBlank String idempotencyKey // NOVO CAMPO
    ) {}
  • Configurar TTL para limpeza automática

  • Testes de duplicação/retry

  • Métricas de operações idempotentes

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions