O que é idempotência?
O interruptor de luz e o botão do elevador
Você entra num quarto escuro e levanta o interruptor. A luz acende. Você levanta de novo, e nada muda, porque a luz já estava acesa. Você pode levantar cinquenta vezes e o resultado é o mesmo: a luz está acesa.
Agora pense num botão de elevador. Você aperta "3" e o elevador começa a se mover. Você aperta "3" de novo. Ele vai ao terceiro andar duas vezes? Não. O elevador reconheceu a sua intenção e ignorou o toque repetido.
As duas são operações idempotentes. Fazer uma vez tem o mesmo efeito que fazer várias vezes, e o sistema chega ao mesmo estado não importa quantas vezes a ação aconteça.
Em software, idempotência quer dizer que chamar uma operação uma vez produz o mesmo resultado que chamá-la duas, três ou cem vezes. O sistema não quebra, não duplica e não corrompe nada quando a mesma requisição chega mais de uma vez.
Por que isso importa no Rails
Num mundo perfeito, toda requisição à sua aplicação Rails acontece uma vez só. O usuário clica em "Pagar", o servidor processa e a resposta chega.
O mundo real é mais bagunçado:
- O usuário dá dois cliques no botão de envio, e duas requisições POST idênticas chegam ao servidor em 200 ms.
- A rede perde a resposta. O navegador nunca recebe a confirmação, então o usuário clica de novo.
- Um background job roda de novo. Um job que gerou erro e é repetido com
retry_on, ou um que um worker liberou ao ser desligado durante um deploy, roda uma segunda vez, mesmo que a primeira tentativa já tenha feito parte do trabalho. - Um webhook chega mais de uma vez. O Stripe reenvia o mesmo evento
payment_intent.succeededquando o seu servidor não responde com um 2xx a tempo.
Se um serviço de pagamento não é idempotente, o usuário é cobrado duas vezes. Se um serviço de matrícula não é, o aluno é matriculado duas vezes e ganha XP em dobro. Se um serviço de email não é, o usuário recebe três emails de boas-vindas. Isso acontece em produção, em qualquer escala.
A definição matemática
Idempotência vem da matemática. Uma função f é idempotente se:
f(f(x)) = f(x)
Aplicar a função duas vezes dá o mesmo resultado que aplicá-la uma vez. Em termos de programação:
# Idempotente: chamar duas vezes produz o mesmo estado
user.update!(active: true)
user.update!(active: true) # Sem mudança — active já era true
# NÃO idempotente: chamar duas vezes dobra o efeito
user.update!(balance: user.balance + 100)
user.update!(balance: user.balance + 100) # Saldo aumentou em 200!
O primeiro exemplo define um valor absoluto, active = true. Não importa quantas vezes você rode, active é true. O segundo usa uma operação relativa, balance + 100, e cada chamada soma mais 100, então o resultado depende de quantas vezes ela roda.
Essa é a distinção central: operações absolutas são naturalmente idempotentes, e operações relativas não são.
Operações idempotentes e não idempotentes no Rails
Veja como isso se aplica a chamadas comuns do Rails:
| Operação | Idempotente? | Por quê |
|---|---|---|
User.find_or_create_by(email: '[email protected]') |
Sim | Cria uma vez, encontra nas próximas |
User.create!(email: '[email protected]') |
Não | Cria uma duplicata, ou gera erro na segunda chamada |
user.update!(role: :admin) |
Sim | Define um valor absoluto, igual toda vez |
user.increment!(:login_count) |
Não | Soma 1 a cada vez, então a contagem só cresce |
Enrollment.find_or_create_by(user: u, course: c) |
Sim | Matricula uma vez, encontra nas próximas |
ExperiencePoint.create!(user: u, amount: 10) |
Não | Cria um registro novo de XP a cada vez |
Operações que definem estado são idempotentes. Operações que acrescentam ou incrementam não são.
Nesta aplicação, essa lógica fica nos service objects. Aqui está um recorte simplificado do Enrollment::CompleteLessonService real; a versão completa também concede XP, atualiza streaks e confere badges, e uma lição mais adiante mostra tudo:
module Enrollment
class CompleteLessonService
def self.call(user:, lesson:)
completion = ::Enrollment::LessonCompletion.new(user: user, lesson: lesson)
# Um índice UNIQUE real em (user_id, lesson_id) torna isso idempotente —
# o save da segunda chamada ou tem sucesso uma vez ou falha com segurança, nunca duplica
completion.save
# ...
end
end
end
Chamar CompleteLessonService.call(user: alice, lesson: lesson_1) dez vezes produz um único registro LessonCompletion, nunca dois. A primeira chamada o cria, e o índice UNIQUE do banco transforma toda chamada seguinte num no-op seguro.
Se uma chamada no-op informa sucesso ou falha é uma decisão de design. O serviço deste app informa falha, para que quem chamou saiba que nada novo aconteceu. A lição sobre o nível do banco volta a essa escolha.
Métodos HTTP e idempotência
A especificação HTTP define quais métodos devem ser idempotentes:
| Método | Idempotente? | Convenção Rails |
|---|---|---|
| GET | Sim | Só leitura; nunca altera dados |
| PUT | Sim | Substitui o recurso inteiro, então o mesmo payload dá o mesmo estado |
| PATCH | Depende | Uma atualização parcial, idempotente quando define valores absolutos |
| DELETE | Sim | Apagar duas vezes deixa o recurso apagado |
| POST | Não | Cria um recurso novo a cada vez |
É por isso que o Rails liga create ao POST e update ao PATCH ou PUT. Criar é, por natureza, não idempotente, porque cada chamada faz um registro novo. Atualizar é naturalmente idempotente quando define valores absolutos.
Eis a armadilha: controllers do Rails não garantem idempotência, então os seus service objects precisam garantir.
Neste app, o botão de matrícula envia um POST para Enrollment::EnrollmentsController#create, que chama Enrollment::StartCourseService. Se esse POST chegar duas vezes, o controller não percebe. O que decide se você terá uma matrícula ou duas é o serviço e o índice único em (user_id, course_id) por trás dele.
Os três níveis de idempotência
Existem três níveis, e conhecê-los ajuda a escolher a abordagem certa:
Nível 1: Idempotência Natural
├── A operação é inerentemente segura para repetir
├── Exemplo: user.update!(active: true)
└── Nenhum código extra necessário
Nível 2: Idempotência Baseada em Constraints
├── Uma constraint do banco de dados previne duplicatas
├── Exemplo: UNIQUE index em (user_id, lesson_id)
└── O banco garante — seu código trata o conflito
Nível 3: Idempotência Baseada em Chave
├── Uma chave única identifica cada operação lógica
├── Exemplo: Idempotency key do cliente (padrão Stripe)
└── Seu código armazena e verifica a chave antes de processar
O nível 1 é de graça: algumas operações são naturalmente seguras, como definir um booleano, substituir uma string ou sobrescrever um timestamp.
O nível 2 usa o banco como garantia. Um índice único em (user_id, lesson_id) faz o próprio PostgreSQL rejeitar um LessonCompletion duplicado. O seu serviço captura a violação da constraint e trata o caso de forma explícita. Alguns serviços devolvem o registro existente como sucesso, e outros informam uma falha para quem chamou saber que nada novo aconteceu. De um jeito ou de outro, o dado nunca duplica.
O nível 3 serve para operações em que o banco não consegue deduzir a unicidade só pelos dados, como pagamentos, requisições de API e webhooks. Quem chama fornece uma chave única, e o servidor confere se essa chave já foi processada.
Este curso cobre os três, então no fim você vai saber qual nível serve para qualquer service object que escrever.
Idempotência nesta arquitetura
Na arquitetura DDD deste app, idempotência é responsabilidade do service object, e não do controller nem do model.
Requisição HTTP (possivelmente um retry)
│
▼
Controller
│ ✅ Passa params para o service
│ ❌ NÃO verifica duplicatas
│
▼
Service Object
│ ✅ Verifica se a operação já aconteceu
│ ✅ Retorna resultado existente se duplicado
│ ✅ Processa normalmente se novo
│ ✅ Retorna Result = Data.define(...)
│
▼
Model + Banco de Dados
│ ✅ Garante constraints (UNIQUE indexes)
│ ✅ Fornece find_or_create_by
│
▼
Result (mesmo na primeira ou décima chamada)
O controller continua enxuto: chama o serviço e renderiza o resultado. O serviço guarda a lógica de idempotência, e o model e o banco dão a rede de segurança. Juntos, fazem requisições duplicadas produzirem respostas duplicadas, nunca efeitos colaterais duplicados.
Erros comuns
O primeiro erro é supor que requisições POST chegam uma vez só. Um usuário dá dois cliques, um webhook é repetido ou um cliente reenvia, e o serviço cria registros duplicados. A regra: todo service object que cria ou altera dados precisa aguentar ser chamado duas vezes com os mesmos argumentos.
O segundo é deixar a deduplicação a cargo do controller. A lógica vaza para o controller, e controllers diferentes que chamam o mesmo serviço se comportam de formas diferentes. A regra: idempotência fica no service object, nunca no controller.
O terceiro é usar increment! ou create! sem uma guarda de idempotência. Contadores contam em dobro, registros contábeis ganham entradas duplicadas, e o XP é concedido duas vezes. A regra: para registros que só crescem, como XP, transações e eventos, use uma idempotency key ou uma constraint única.
Resumo
| Conceito | Ponto-chave |
|---|---|
| Idempotência | Chamar uma operação uma vez dá o mesmo resultado que chamá-la várias vezes |
| Absoluto vs relativo | update!(active: true) é idempotente; increment!(:count) não é |
| Por que importa | Cliques duplos, retries de rede, jobs que rodam de novo e webhooks repetidos causam chamadas duplicadas |
| Métodos HTTP | GET, PUT e DELETE são idempotentes pela especificação; POST não é; PATCH depende da implementação |
| Três níveis | Natural (de graça), por constraint (índice UNIQUE), por chave (idempotency keys) |
| Onde fica | No service object, com o banco como rede de segurança |
| Padrão Rails | find_or_create_by é o padrão idempotente mais simples: cria uma vez e encontra nas próximas |
Continue com a aula 2
O custo de código não-idempotente
Mostra como cliques duplos, retries de jobs, webhooks repetidos e a corrida do find_or_create_by criam duplicatas silenciosas.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.