💧📆🏛️ Um guia para uma arquitetura orientada a eventos em Elixir
Tags: elixir, braziliandevs, eventdriven, architecture
Published: 2022-05-31 11:33:58

Esta é uma tradução do post [A Guide to Event-Driven Architecture in Elixir](https://blog.appsignal.com/2022/05/10/a-guide-to-event-driven-architecture-in-elixir.html?utm_source=elixir-radar-sponsored&utm_medium=email&utm_campaign=2022-05-10) escrito por [Sapan Diwakar](https://blog.appsignal.com/authors/sapan-diwakar).

Também acrescentei algumas imagens para exemplificar melhor a explicação. Na dúvida sobre a tradução visite o post original. Vamos ao post.

[![A Jornada do Autodidata em Inglês](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkoqkmh9hic17tnddjf6.png)](https://go.hotmart.com/R93865620U)

Neste post, exploraremos como a __arquitetura orientada a eventos (event-driven architecture)__ pode tornar sua aplicação mais responsiva aos usuários e desacoplar seus módulos para uma melhor experiência do desenvolvedor.

Também analisaremos vários métodos de implementação de arquitetura orientada a eventos com Elixir. A linguagem Elixir é particularmente boa para isso devido às APIs de passagem de mensagens avançadas e concisas que ela oferece e ao excelente suporte da BEAM para a concorrência.

Mas primeiro: o que é exatamente uma arquitetura orientada a eventos?



* [Arquitetura orientada a eventos: Uma Introdução](#arquitetura)
* [Construindo blocos de uma arquitetura orientada a eventos](#construindo-blocos)
* [Benefícios da arquitetura orientada a eventos](#beneficios)
* Abordagens
 - [Arquitetura síncrona orientada a eventos em Elixir](#sincrona)
 - [Usando o GenServer na arquitetura orientada a eventos em Elixir](#genserver)
 - [Implementação orientada a eventos usando GenStage em Elixir](#genstage)
* [Conclusão: Arquitetura orientada a eventos em Elixir - Indo além do GenStage](#embrulhe)



# Arquitetura orientada a eventos: Uma Introdução <a name="arquitetura">

A arquitetura orientada a eventos é uma __arquitetura onde os eventos controlam o comportamento e o fluxo de sua aplicação__. Os principais componentes da arquitetura são os produtores de eventos `(event producers)`, o barramento de eventos `(event bus)` e os consumidores de eventos `(event consumers)`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rzf1tbygi002oblpusq9.png) 

Um evento pode ser __qualquer coisa que represente uma mudança de estado no sistema__. Por exemplo, em uma aplicação de comércio eletrônico ‘e-commerce’, a compra de um produto pelo usuário poderia produzir um __evento de venda__ ao qual o consumidor poderia então iniciar um processo de atualização de estoque.

Uma arquitetura baseada em eventos permite que as aplicações atuem em eventos à medida que eles ocorrem. Diferentes partes de uma aplicação funcionam e se desenvolvem de forma relativamente independente em um projeto bem elaborado, baseado em eventos. As organizações podem designar equipes separadas para partes focadas da aplicação e racionalizar o fluxo de trabalho. Isto também cria uma fronteira clara entre diferentes partes de uma aplicação, auxiliando em futuros exercícios de escalabilidade.

A arquitetura orientada por eventos ganhou popularidade principalmente com produtos baseados em microserviços, mas também pode ser usada para um monólito.

As coisas são sempre mais claras com um exemplo, então vamos olhar para um.

# Construindo blocos de uma arquitetura orientada a eventos <a name="construindo-blocos">

Vamos discutir cada bloco de construção em detalhes, usando uma aplicação de ‘e-commerce’ como exemplo.

Imagine que um usuário faz uma nova compra em um ‘website’. O novo pedido é um evento gerado pela parte que controla o pedido: o __event producer__.

O evento pode ser empurrado para um __event bus__. O __event bus__ pode ser qualquer coisa, como, por exemplo:

* Uma tabela no banco de dados.
* Uma fila de eventos em memória dentro da aplicação.
* Uma ferramenta externa como RabbitMQ ou Apache Kafka.

Os __event producers__ interessados neste tipo de evento podem se inscrever no __event bus__. O evento é entregue-lhes, e eles fazem algum processamento em cima dele.

Por exemplo, um sistema de gerenciamento de estoque assinaria (subscribe) o novo evento de pedido e atualizaria o estoque do produto. Outro sistema também poderia escolher o mesmo evento no pedido - por exemplo, um serviço de atendimento poderia processar esse evento e criar uma rota de entrega para o produto.


# Benefícios da arquitetura orientada a eventos <a name="beneficios">

Há várias vantagens em utilizar uma __arquitetura orientada a eventos__ ao invés de uma que __processa tudo sequencialmente__.

Uma arquitetura orientada a eventos nos permite construir __várias partes independentes__ de uma aplicação para trabalhar com o __mesmo evento e realizar diferentes tarefas focadas__. Isto pode ser vantajoso por um par de razões:

* Uma equipe precisa se concentrar apenas em uma parte.
* O código de aplicação para partes pequenas pode ser simples.

Outra vantagem do conceito é que ele nos permite fornecer uma ‘interface’ rápida ao usuário. Como exemplo da aplicação acima, um usuário precisa fazer um pedido. A aplicação pode continuar processando todas as outras tarefas não-interativas, como atualizar seu inventário e interagir com a aplicação de entrega, sem a atenção do usuário.

Isto também torna muito fácil acrescentar novas etapas de processamento no pipeline do evento. Por exemplo, digamos que precisamos de uma tarefa adicional a ser realizada a partir de um evento. Só precisamos adicionar um novo consumidor para lidar com o evento, sem tocar em nenhuma outra parte da aplicação.

Finalmente, é muito mais fácil escalar cada módulo individual se ele for desacoplado dos outros, do que escalar uma aplicação inteira em conjunto. Isto é ainda mais benéfico ao ter uma parte que consome muito mais recursos do que suas contrapartes no pipeline de processamento de eventos.

Mas a arquitetura orientada a eventos não vem sem desvantagens quando não é bem pensada. Se aplicada a problemas muito simples, ela pode levar a fluxos de trabalho complexos que são lentos e difíceis de depurar.

Vamos explorar algumas maneiras simples de implementar uma arquitetura orientada a eventos com Elixir, sem a necessidade de escrever partes complexas de código.


# Arquitetura síncrona orientada a eventos em Elixir <a name="sincrona">

A maneira mais simples __(e mais ineficiente)__ de executar o fluxo acima seria __fazer tudo de forma síncrona__ no pedido do usuário.

Portanto, se você tiver um módulo de __Pedidos (Orders)__ que processe o pedido de um usuário, a __implementação síncrona__ poderia se parecer com isto:

```elixir
defmodule Orders do
  def create_order(attrs) do
    # salva o pedido
    {:ok, order} = save_order(attrs)
    # atualiza o estoque
    {:ok, _inventory} = update_inventory(order)
    # cria a entrega
    {:ok, _delivery} = create_delivery(order)
    # retorna o pedido
    {:ok, order}
  end
end
```

Podemos melhorar isto para que os consumidores de novos eventos possam escalar mais facilmente:

```elixir
defmodule Orders do
  @event_consumers [
    {Inventory, :handle_event},
    {Delivery, :handle_event},
  ]
 
  def create_order(attrs) do
    {:ok, order} = save_order(attrs)
 
    event = %Orders.Event{type: :new_order, payload: order}
    @event_consumers
    |> Enum.each(fn {module, func} ->
      apply(module, func, [event])
    end)
 
    {:ok, order}
  end
end
```

Com esta implementação, tudo o que precisamos fazer para adicionar novos consumidores é adicionar a especificação na lista `@event_consumers`, e esses consumidores podem trabalhar independentemente.

Embora a abordagem síncrona funcione bem para um pequeno número de consumidores, ela tem uma desvantagem. A criação de um pedido pode levar muito tempo porque será necessário esperar a atualização do estoque e a criação da entrega.

Estas são tarefas internas que podem ser realizadas __sem interação do usuário__ e movidas da cadeia síncrona.

Vamos ver como podemos racionalizar ainda mais a arquitetura orientada a eventos usando o [GenServer](https://hexdocs.pm/elixir/1.13/GenServer.html).


# Usando o GenServer na arquitetura orientada a eventos em Elixir <a name="genserver">

Para esta implementação, executaremos um processo separado para cada consumidor. Estes se inscreverão num evento de um produtor e executarão suas tarefas simultaneamente.

Para o __event bus__, podemos utilizar o [Phoenix.PubSub](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html). Note que é possível para aplicativos que não usam Phoenix [usar diretamente o Registro como um PubSub](https://hexdocs.pm/elixir/Registry.html#module-using-as-a-pubsub).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4dfjsl7ud4e210mlbem.png) 

Primeiro, vamos olhar para o produtor.

```elixir
defmodule Orders do
  def create_order(attrs) do
    {:ok, order} = save_order(attrs)
    event = %Orders.Event{type: :new_order, payload: order}
    Phoenix.PubSub.broadcast(:my_app, "new_order", event)
    {:ok, order}
  end
end
```

Em um novo pedido (order), criamos a estrutura do evento e usamos o `Phoenix.PubSub.broadcast/3` para transmitir esse evento no barramento (bus). Como você pode ver, é muito mais simples do que a implementação anterior, onde o módulo de `Orders` processava tarefas do outro módulo em série.

Os consumidores podem então assinar (subscribe) o tópico `new_order` e implementar o `handle_info/2` para serem notificados toda vez que um novo evento é publicado pelo produtor.

```elixir
defmodule Inventory do
  use GenServer
 
  def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
 
  def init(_opts) do
    Phoenix.PubSub.subscribe(:my_app, "new_order")
  end
 
  def handle_info(%Orders.Event{type: :new_order, payload: order}, state) do
    state = consume(state, order.product)
    {:noreply, state}
  end
end
```

O módulo de `Delivery` será muito semelhante ao acima mencionado, por isso estou pulando aqui.

Como você pode ver, isto é muito melhor do que as implementações anteriores. Os módulos de `Inventory` e `Delivery` podem se inscrever independentemente para o tópico `new_order`.  O módulo `Orders` transmite (broadcasts) para este tópico sobre novos pedidos e eventos são entregues para os processos inscritos.

Você pode até distribuir isto entre vários nós e Phoenix.PubSub (com um PG, Redis ou outro adaptador), espalhando os eventos para todos os nós.

Ótimo, certo? Na verdade, não. Há vários problemas com esta abordagem:

* O PubSub fornece uma transmissão em tempo real sem fila de mensagens, portanto, se um dos processos do assinante estiver em suspenso, ele pode perder as transmissões.

* Se o assinante fizer algum trabalho pesado, ele pode não conseguir acompanhar as mensagens recebidas, resultando em timeouts e, conseqüentemente, em um colapso da árvore de processos.

* Se o assinante encontrar um erro no processamento de uma mensagem, ela é considerada consumida e não será novamente processada (retried) mais tarde.

Portanto, esta abordagem é uma má abordagem a ser seguida para nosso caso de uso atual.

Entretanto, a abordagem ainda tem seus casos de uso. Ela pode ser usada para tarefas que não são críticas, ou que podem ser corrigidas com a próxima mensagem: por exemplo, se uma tarefa computar as sugestões para as próximas compras de um usuário com base em sua última compra. Embora isto também precise ser acionado em um novo pedido, não é exatamente crítico (para um site tradicional de e-commerce) e pode recalcular as sugestões para um usuário em sua próxima compra.

# Implementação orientada a eventos usando GenStage em Elixir <a name="genstage">

Na seção anterior, vimos uma grande implementação de nosso sistema orientado a eventos utilizando o GenServer. Mas ele não veio sem suas limitações. Vamos ver como o [GenStage](https://hexdocs.pm/gen_stage/GenStage.html) se comporta.

O __GenStage__ faz uma distinção clara entre `producers`, `consumers` e `producer_consumers`, e cada processo tem que escolher um quando começa (em seu `init/1`). Em nosso caso, tanto o `Inventory` quanto o `Delivery` são `consumers`, e `Orders` é um `producer`.

É aqui que as coisas começam a ficar um pouco complicadas. O GenStage tem um conceito de demanda. Cada `consumer` pode emitir uma demanda de quantos eventos ele pode lidar. O `producer` precisa enviar esses eventos para o `consumer`. Vamos ver um `producer` básico em ação.

```elixir
defmodule Orders do
  use GenStage
 
  def start_link(opts) do
    GenStage.start_link(__MODULE__, opts, name: __MODULE__)
  end
 
  def init(_opts) do
    {:producer, :some_state_which_does_not_currently_mattere}
  end
 
  def create_order(pid, attrs) do
    GenStage.cast(pid, {:create_order, attrs})
  end
 
  def handle_cast({:create_order, attrs}, state) do
    {:ok, order} = save_order(attrs)
    {:noreply, [%Orders.Event{type: new_order, payload: order}], state}
  end
 
  def handle_demand(_demand, state), do: {:noreply, [], state}
end
```

A carne de nosso código está em `handle_cast`, onde guardamos o pedido (order) e devolvemos uma tupla como `{:noreply, events, new_state}`. Os novos eventos são armazenados em um buffer __GenStage__ interno e enviados aos consumidores à medida que eles fazem novas demandas (ou imediatamente, se houver consumidores com demanda não atendida).

Vamos checar uma amostra da implementação do `consumer`:

```elixir
defmodule Inventory do
  use GenStage
 
  def start_link(opts) do
    GenStage.start_link(__MODULE__, opts, name: __MODULE__)
  end
 
  def init(_opts) do
    {:consumer, [], subscribe_to: [Orders]}
  end
 
  def handle_events(events, _from, state) do
    state = Enum.reduce(events, state, & handle_event(&1, &2))
    {:noreply, [], state}
  end
 
  def handle_event(%Orders.Event{type: :new_order, payload: order}, state) do
    new_state = update_inventory(order)
    new_state
  end
end
```

No `consumer`, primeiro avisa de que temos um `subscribe_to` (inscrição_para) dentro do `init/1`. Isto automaticamente assina/inscreve (subscribe) o `Inventory` para qualquer evento publicado por `Orders`. Favor verificar a documentação [GenStage para opções adicionais disponíveis no init](https://hexdocs.pm/gen_stage/GenStage.html#c:init/1).

Aqui, a maior parte do trabalho acontece dentro do `handle_events/3`, que é automaticamente chamado pela __GenStage__ assim que novos eventos se tornam disponíveis. Tratamos aqui do evento `new_order`, atualizando o `inventory` e retornando um novo estado.

Com esta simples implementação, obtemos vários benefícios que superam a implementação do GenServer:

* Armazenamento automático de eventos dentro do buffer interno do GenStage quando o `producer` tem novos eventos sem nenhum `consumer` disponível.

Mesmo que os `consumers` estejam ausentes quando alguns eventos são produzidos, nós ainda temos a garantia de recebê-los quando o `consumer` volta a ficar disponível.

[Confira o guia da Genstage sobre a demanda de buffering](https://hexdocs.pm/gen_stage/GenStage.html#module-buffering-demand) para uma lógica de buffering avançada.

* Distribuição automática de trabalho em múltiplos consumidores.

Se você tem tarefas pesadas de consumo, você pode iniciar múltiplos processos de consumo. O [DemandDispatcher](https://hexdocs.pm/gen_stage/GenStage.DemandDispatcher.html) padrão para GenStage distribuirá o trabalho uniformemente por todos os processos.

Veja o [GenStage.Dispatcher](https://hexdocs.pm/gen_stage/GenStage.Dispatcher.html) para outras estratégias de despacho para [distribuir eventos a todos os consumidores](https://hexdocs.pm/gen_stage/GenStage.BroadcastDispatcher.html) ou [distribuição de partições aos consumidores com base em uma função hash](https://hexdocs.pm/gen_stage/GenStage.PartitionDispatcher.html).

Mas, como no __GenServer__ ou na __implementação síncrona__, o uso do GenStage não vem sem seus problemas.

Se um `consumer` se chocar enquanto estiver processando um evento, o GenStage considerará o evento entregue e não o enviará novamente quando o `consumer` voltar a ficar disponível.

Para ter certeza de que você rastreia corretamente os acidentes, você pode usar um serviço de monitoramento como o [AppSignal](https://www.appsignal.com/). O AppSignal é [fácil de instalar para sua aplicação Elixir] e ajuda a monitorar o desempenho, assim como [rastrear erros](https://www.appsignal.com/tour/errors). Aqui está um exemplo de um painel de controle de erros que o AppSignal fornece:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shksgj4uma7pqlyr58kl.png)

Você também pode configurar notificações de colisões via AppSignal.

No lado do aplicativo, você pode armazenar tais eventos em uma loja persistente uma vez que eles sejam entregues ao consumidor (consumer). Se o consumidor se chocar, então uma vez que se recupere, pode retornar aos eventos em cache.

Seja muito cauteloso em produzir muitos eventos sem consumidores suficientes, no entanto. Enquanto a GenStage oferece um buffer automático de eventos, este buffer tem um tamanho máximo (configurável) e um tamanho máximo prático limitado pela memória do servidor.

Se você não controla a freqüência com que os eventos são produzidos, considere o uso de um armazenamento de dados externo como Redis ou Postgres para fazer o buffer de eventos.

# Conclusão: Arquitetura orientada a eventos em Elixir - Indo além do GenStage <a name="conclusao">

Neste post, examinamos três abordagens para implementar um sistema orientado a eventos no Elixir: de forma síncrona, usando o GenServer, e finalmente, usando o GenStage. Examinamos algumas das vantagens e desvantagens de cada abordagem.

O exemplo simples do __GenStage__ pode ser um ponto de partida para a implementação de complexos pipelines de processamento de dados orientados por eventos que abrangem vários nós. Sugiro que você leia a grande [documentação do GenStage para obter mais informações](https://hexdocs.pm/gen_stage/GenStage.html).

Se você estiver procurando por uma abstração ainda maior, a [Broadway](https://elixir-broadway.org/) é um bom ponto de partida. Ela é construída sobre o GenStage e oferece várias características adicionais, incluindo o consumo de dados de filas externas como Amazon SQS, Apache Kafka, e RabbitMQ.

Até a próxima vez, boa codificação!