America/Sao_Paulo
Posts

Idempotência: Importância e papel na construção de aplicações resilientes

January 29, 2025
Cover foto.
Se você trabalha com desenvolvimento de software, provavelmente já se deparou com o conceito de idempotência. Mas o que ele significa e qual sua importância na construção de aplicações resilientes a falhas? Vamos explorar isso juntos!
Uma operação é considerada idempotente quando sua execução repetida produz o mesmo estado final na aplicação, independentemente do número de vezes que for chamada. Isso não significa que a resposta será sempre a mesma, mas sim que chamadas subsequentes não terão efeitos colaterais adicionais. Por exemplo, uma requisição DELETE em um recurso de uma API REST, se corretamente implementada, deve remover o recurso na primeira execução e retornar um status 204 No Content. Já as chamadas subsequentes devem retornar um 404 Not Found, pois o recurso já não existe, mantendo o estado da aplicação inalterado após a primeira execução. As operações em APIs REST como GET, PUT, HEAD e o OPTIONS, se implementadas corretamente, também devem ser idempotentes, enquanto outras como POST não, pois a cada requisição é esperado que um novo recurso seja criado, gerando efeitos colaterais. O método PATCH no entanto é alvo de controvérsias quanto a presença ou não de idempotência. Como fica a critério do desenvolvedor atribuir o comportamento de cada chamada HTTP em uma API o que foi discutido nem sempre é a regra.
Ao projetar sistemas resilientes a falhas pode ser necessário considerar estratégias de retentativa em alguns fluxos. Para isso, é importante que as operações efetuadas não levem a aplicação à estados inconsistentes. Imagine ter recebido um erro ao realizar um pagamento e, ao tentar novamente, descobrir que o valor foi debitado da sua conta duas vezes? Ou então possuir uma aplicação que escuta as mensagens de uma fila utilizando o Amazon SQS, que pode ocasionalmente entregar uma mensagem mais de uma vez? É preciso ter certeza que é seguro realizar uma retentativa e é ai que entra a importância da idempotência! Ao chamar um método idempotente mais de uma vez não precisamos nos preocupar com estados inconsistentes gerados por retentativas, pois sabemos que não haverão efeitos colaterais nas chamadas subsequentes. Por isso é importante conhecer estratégias para implementação de idempotência mesmo em casos onde ela não é tida por padrão como no método POST que mencionamos anteriormente e é isso que abordaremos a seguir.
Em alguns casos, pode ser necessário tornar um método POST idempotente. Não gostaríamos de criar mais de uma conta ao fazermos múltiplas requisições idênticas para o servidor, não é mesmo? Existem alguns métodos que podem ser aplicados para tornar um método POST idempotente, como por exemplo o uso de uma chave de idempotência. A chave de idempotência é um campo que podemos adicionar no recurso e que deve ser preenchido com informações que tornam aquele recurso único. No caso de criação de uma conta citado acima poderíamos usar o CPF como chave de idempotência. Para garantir que dois recursos não tenham a mesma chave de idempotência podemos criar uma regra na camada da aplicação ou então tornar aquele campo único na camada do banco de dados, evitando duplicações. Quando não temos um campo óbvio que torne aquele recurso único como no caso do CPF uma solução possível é o uso de uma hash do recurso criada com os valores dos campos de interesse que tornam o recurso único. Mas é preciso tomar cuidado quando utilizamos essa alternativa, pois é possível que haja colisões de hash, impedindo a publicações de recursos mesmo que eles sejam diferentes. Quando a idempotência precisa ser garantida apenas por um intervalo determinado de tempo podemos usar um banco de dados em memória como o redis para armazenar a chave de idempotência, especificando um tempo de expiração para esse valor. Dessa forma temos menos chances de criar um problema devido á uma colisão de hash.
Para exemplificar como é possível implementar as estratégias citadas criei um repositório no github contendo o código de uma aplicação de exemplo em Kotlin utilizando Spring Boot 3.4.2.
O projeto é bem simples e para executa-lo basta ter instalado o java na versão 21 e o docker e seguir as seguintes instruções:
  1. Clone o repositório
git clone https://github.com/daanrsantiago/idempotency-example
  1. Navegue até o diretório do projeto:
cd idempotency-example
  1. Compile o projeto usando o gradle
./gradlew build
  1. Execute o docker compose
docker compose up
  1. Execute a aplicação
java -jar build/libs/idempotency-example-0.0.1-SNAPSHOT.jar
Após a inicialização podemos acessar http://localhost:8080/swagger-ui.html pelo navegador para começar a interagir com a aplicação via a interface do swagger-ui. O resultado deve ser algo como o a seguir:
Pagina Swagger-UI.
A partir daí fica simples interagir com os recursos da aplicação.
A aplicação define dois recursos não relacionados: Publication e User. As suas representações no banco de dados são definidas pelos arquivos a seguir:
Publication.kt
package dev.danielsantiago.idempotency.model

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType.AUTO
import jakarta.persistence.Id
import java.time.LocalDateTime

@Entity
data class Publication(
  @Id
  @GeneratedValue(strategy= AUTO)
  var id: Long? = null,
  var title: String,
  var content: String,
  val creationDate: LocalDateTime = LocalDateTime.now()
)
User.kt
package dev.danielsantiago.idempotency.model

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType.AUTO
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name = "users")
data class User(
  @Id
  @GeneratedValue(strategy = AUTO)
  var id: Long? = null,
  val name: String,
  val age: Int,
  @Column(unique = true)
  val cpf: String
)

Repare que o na definição da coluna cpf da classe User foi usado o parâmetro unique=true. Isso faz com que possa haver apenas usuários com valores de cpf distintos, na prática o cpf passa a atuar como uma chave de idempotência. Ao tentar salvar dois usuários com o mesmo valor de cpf é gerada uma exceção na camada de banco de dados, que o controller reconhece e retorna um erro 422 para o cliente. Agora vamos analisar o controller do recurso Publication
Publication.kt
package dev.danielsantiago.idempotency.controller

import ...

@RestController
@RequestMapping("/publication")
class PublicationController(
  val publicationRepository: PublicationRepository,
  val template: RedisTemplate<String, String>
) {
  val PREFIX = "publication:idempotency:"

  ...

  @PostMapping()
  fun publishPublication(@RequestBody body: PublicationRequest): ResponseEntity<Publication> {
      val publishedPublication = publicationRepository.save(
          Publication(
              title = body.title,
              content = body.content
          )
      )
      return ResponseEntity.status(CREATED).body(publishedPublication)
  }

  @PostMapping("/idempotent")
  fun eventualIdempotency(@RequestBody body: PublicationRequest): ResponseEntity<Publication> {
      if(template.opsForValue().setIfAbsent(PREFIX + body.hashCode(), "1", Duration.ofMinutes(1))!!) {
          val publishedPublication = publicationRepository.save(
              Publication(
                  title = body.title,
                  content = body.content
              )
          )
          return ResponseEntity.status(CREATED).body(publishedPublication)
      } else {
          return ResponseEntity.status(UNPROCESSABLE_ENTITY).build()
      }
  }
  ...
}
Foram definidos dois mapeamentos POST, um no path /publication que permite duplicações, que portanto não é idempotente, e outro no path /publication/idempotent, que não permite duplicações durante um período de um minuto, e que portanto é idempotente nesse período. Quando a aplicação recebe uma requisição no path /publication/idempotent ela tenta armazenar um valor na chave "publication:idempotency:{hashDaPublicação}" com um período de expiração de 1 minuto em nossa instância do redis. Caso esse valor não exista, a aplicação cria esse valor e salva a publicação com sucesso, caso contrário a publicação não é salva e é retornado um status 422 para o cliente. Essa implementação permite que apenas publicações com informações diferentes possam ser criadas dentro de um período de 1 minuto, desde que não hajam colisões no hash criado. Isso pode ser interessante para fazer uma especíe de debounce de uma requisição no servidor.
A idempotência é um conceito fundamental para o desenvolvimento de aplicações confiáveis e resilientes. Ao garantir que operações repetidas não causem efeitos inesperados, podemos projetar sistemas mais robustos e seguros. Implementar idempotência exige planejamento e boas práticas, mas os benefícios em termos de consistência e confiabilidade fazem com que esse esforço valha a pena.