Transactional outbox

Como acabar com a escrita dupla

Gustavo Freze
5 min readAug 2, 2022

Antes de falar do padrão Transactional outbox, vamos entender o problema que nos leva a usá-lo.

O que é escrita dupla/dual write?

Uma gravação dupla descreve a situação de quando você precisa alterar dados em 2 sistemas, sem utilizar uma camada adicional que garanta a consistência dos dados em ambos os serviços.

E por que isso é um problema?

Desde que ambas as operações sejam bem-sucedidas, tudo está bem. Mas, se uma das operações falhar, seu sistema está agora em um estado inconsistente e não há uma maneira fácil de corrigi-lo.

Para ilustrar o problema, criei um pequeno caso de uso, onde o foco é a aplicação transaction. Ela precisa receber um comando RequestTransaction, que após processado, resulta em um evento TransactionRequested, indicando que a transação foi solicitada. Esse comando é resultante do evento de CheckoutDone, da aplicação checkout.

Figura 01: Implementação do caso de uso.

Se implementarmos o caso de uso como na figura acima, estamos suscetíveis a duas possibilidades de falhas, que são inerentes à escrita dupla. Podemos ter uma falha ao escrever no banco de dados, ou, ao enviar o evento para o broker.

Figura 02: Possibilidade de falha inerente da escrita dupla.

Possíveis implementações (ou não)

Enviar a notificação para RabbitMQ, persistir a transação no banco de dados e “comitar”.

Figura 03: Fluxo da solução.

O problema dessa abordagem, é que ao enviar o evento primeiro, e caso este ocorra com sucesso, no momento em que for solicitado a persistência no banco de dados, se uma falha ocorrer, não teremos mais o evento do nosso lado, de maneira que acabamos de gerar uma inconsistência.

Figura 04: Fluxo da solução com erro.

Persistir a transação no banco de dados e “comitar”, e enviar a notificação para o RabbitMQ.

Figura 05: Fluxo da solução.

O problema dessa abordagem, é que por mais que agora garantirmos ter o evento do nosso lado antes de enviá-lo, ainda não podemos garantir a consistência do fluxo, pois, ainda estamos suscetíveis a falhas no envio do evento para o broker.

Figura 06: Fluxo da solução com erro.

Porém, como o evento está salvo, temos a possibilidade de corrigir esta falha, com algumas implementações adicionais.

Podemos adicionar uma flag em nossa tabela do banco de dados, para indicar se o evento foi publicado com sucesso. E ao fazer o envio do respectivo evento, atualizamos a flag. Posteriormente, implementamos um Cron, para republicar todos os registros que não foram publicados com sucesso.

Figura 07: Contornando o problema.

É uma solução possível, porém, ela adiciona uma complexidade ao nosso fluxo que não é interessante.

Transactional outbox

O que é ?

O Transactional outbox, é uma técnica de garantia de entrega utilizada comumente na troca de mensagens entre sistemas ou partes de um sistema, quando não podemos ter atomicidade na operação.

Como ele resolve o problema?

O padrão basicamente nos diz, que ao invés de tentar uma ação direta não-atômica (publicação do evento/mensagem), devemos utilizar operações atômicas que posteriormente nos permitam a execução da ação desejada.

Podemos atingir esse objetivo criando uma tabela no banco de dados chamada ‘outbox’, onde registramos os eventos que devem ser publicados. Então em uma mesma transação do banco de dados, vamos realizar as duas escritas, e posteriormente, será realizado a sua publicação no nosso message broker.

Figura 08: Fluxo com Transactional outbox.

Exemplo de campos para a tabela de outbox:

  • id: UUID que identifica o evento no formato canônico (separado por hífen na forma 8–4–4–4–12).
  • aggregate_type: Nome da raiz de agregação que produziu o evento no formato CamelCase.
  • aggregate_id: Representação textual do identificador da raiz de agregação.
  • event_type: Nome do evento no formato CamelCase.
  • revision: Número positivo que indica a versão do payload produzido do evento.
  • payload: Payload do evento como um objeto json.
  • occurred_on: Momento em que o evento ocorreu.
  • created_at: Data em que o registro foi inserido.

Implementando o caso de uso com Transactional outbox

Baseado no caso de uso acima, criei uma POC que implementa o Transactional outbox. Você pode conferir a solução completa neste repositório do github.

FAQ

Por que utilizou o Kafka ao invés do RabbitMQ, como nos exemplos?

Deixando de lado qualquer outra comparação, basicamente o Kafka dispõe de recursos e componentes que facilitam a implantação do Transactional outbox. Por exemplo, o Kafka Connect, é um componente do Kafka, com ele podemos criar conectores que vão mapear nossa tabela de outbox, e cada inserção nessa tabela, através do binlog (ou recurso análogo a ele, varia conforme o banco de dados) teremos a publicação do registro em um tópico pré determinado.

Porém, se optar pelo RabbitMQ, você só precisar criar uma implementação que extraia esses dados da tabela de outbox, e envie para uma fila no RabbitMQ.

Por que não abrir uma transação no banco, enviar o evento, e se o envio ocorrer com sucesso, fazer um commit da transação?

Bom, apesar de parecer uma proposta lógica, ela esconde alguns problemas.

  • O primeiro ponto, é o erro de design, em que você está inferindo que o ciclo de vida de uma aplicação interfira e dependa diretamente de uma aplicação externa.
  • O segundo ponto, é que se o commit falhar, você já fez o envio para o broker, gerando inconsistência.

--

--