Arquitetura Event-Driven usando AsyncAPI na prática
José Roberto Araújo
Posted on May 20, 2024
No mundo atual, acelerado e interconectado, os dados se tornaram a força vital das empresas. Como resultado, a necessidade de uma gestão e integração de dados eficazes nunca foi tão grande. É aqui que a Arquitetura Orientada a Eventos (EDA) e a AsyncAPI entram em ação.
EDA é um estilo arquitetural que enfatiza a o funcionamento assíncrono de sistemas distribuídos, reagindo a eventos. Ele permite o processamento de dados em tempo real e ajuda sistemas a se tornarem mais responsívos e ágeis. No entanto, gerir e governar a complexidade desse estilo de arquitetura pode se tornar um grande um desafio. É aqui que entra a AsyncAPI.
AsyncAPI é uma padrão de especificação, de código aberto, que descreve APIs orientadas a eventos. Ele fornece uma maneira padronizada de documentar e comunicar a estrutura e o comportamento de sistemas orientados a eventos. Ao adotar o uso do padrão AsyncAPI, as empresas ganham melhorias sobre a visibilidade dos contratos de mensagens, produtores e consumidores, melhora a governança sobre os dados que estão trafegando dentro da plataforma e aumenta a capacidade de identificação de impactos sobre mudanças.
Nesse post vou usar uma aplicação fictícia de reservar de quartos de hotel, afim de trazer um cenário mais prático e facilitar o entendimento de como aplicar o AsyncAPI em projetos no seu dia a dia.
Caso de uso 📝
Em um sistema de reserva de quartos de hotel, quando um cliente efetua a reserva de um quarto no hotel, esse espaço precisa ser atualizado em toda a plataforma, indicando que a quantidade desse tipo de opção precisa ser decrementada ou não existe mais essa opção disponível para reserva, dada a data selecionada pelo cliente da aplicação.
Imagine que você está desenhando uma solução de comunicação assíncrona para atender ao seguinte caso de uso: Atualizar o sistema quando um quarto for reservado!
Perceba que para realizar a implementação desse requisito será necessário envolver 3 áreas distintas dentro da plataforma, as quais detém a propriedade de 3 microsserviços diferentes: Reserva, Administração Hotel e Pesquisa.
Quando o serviço de Reservas recebe a requisição para confirmar a reserva, é então publicada uma mensagem (neste caso um evento) do tipo ReservaQuartoConfirmada, o serviço de Administração Hotel reage ao evento que foi publicado e atualiza as informações do quarto em seu sub-domínio, o que faz, por sua vez, provocar a publicação de uma outra mensagem diferente, informando que o quarto X foi atualizado, o que por sua vez vai acionar o serviço de Pesquisas que tem a responsabilidade de atualizar a sua fonte de dados, indicando ou a diminuição da quantidade de quartos Xs disponíveis ou remover ele da lista de pesquisa.
Analisando o cenário cima, percebe-se que uma simples ação de reservar um quarto no hotel, vai acionar vários serviços diferentes, responsáveis por atualizar seus respectivos sub-domínios, reagindo à um evento específico dentro da plataforma.
O problema ⚠️
Após ter desenhado, apresentado e validado o design acima, já temos em mãos o desenho da solução pronto para seguir a diante. Certo!?
Não tão rápido assim, como você pensa! 🙂
Em arquitetura de software não devemos, simplesmente, ter um desenho de solução em mãos e seguir para a implementação, antes de verificarmos outros estágios e passos, que nos guiarão durante a fase mais divertida para todos nós desenvolvedores: Realização da solução através de código. 💪🏻
Analisando o contexto da solução, fica fácil percerber que a existência de 3 serviços distintos, provavelmente, também vai nos conduzir para uma tipologia de times onde normalmente temos 3 times diferentes, responsáveis por atuar com cada um desses serviços em separado.
Microsserviços está diretamente relacionado com a parte estratégica do Domain-Driven Design. Contextos Delimitados (Bounded Contexts) são conceitos fundamentais do DDD, dando essencial suporte para tomadas de decisões sobre as fronteiras de cada um dos serviços, e isso também define quais informações são publicadas de um sub-dominio para o meio externo, seja por meio de APIs internas ou seja, no nosso caso aqui desse post, por meio de mensagem em plataformas de mensageria.
Se quiser saber mais sobre como modelar aplicações usando o DDD de forma estratégica, reserve um espaço na sua agenda para ler essa série de artigos aqui.
Na solução proposta no diagrama acima, temos 2 tipos diferentes de mensagens: ReservaQuartoConfirmada e QuartoAtualizado. O contrato dessas mensagens precisa estar consistente e coerente para que ambos os times possa se comunicar de forma eficaz e efetiva, sem ter surpresas por uma eventual mudança, sem haver comunicação entre os times.
Mas como se alcança algo tão organizado quanto o que se tem usando OpenAPI?
Um processo adequado deve estar implementado para descrever os diferentes componentes de um sistema orientado a eventos e suas interações.
A especificação AsyncAPI entra em ação neste ponto.
AsyncAPI specification FTW 🏆
Abaixo está uma citação oficial do site AsynAPI, onde eles definem o que é e para que serve essa especificação:
AsyncAPI is an open-source initiative that seeks to improve the current state of Event-Driven Architectures (EDA). Our long-term goal is to make working with EDAs as easy as working with REST APIs. That goes from documentation to code generation, from discovery to event management, and beyond.
O padrão AsyncAPI tomou como base muitas das definições e conjunto de dados, presentes dentro da especificação OpenAPI. Dessa forma, a sua curva de aprendizado será muito baixa. Atualmente a versão mais recente do AsyncAPI é a 3.0.
Documentanto a solução arquitetural
Vamos começar a documentação da solução arquitetural, usando as especificações do AsyncAPI 3.0. O objetivo desse processo é criar especificações dos contratos das mensagens e compartilhar essa informação de forma global dentro da empresa. Dessa forma, os times terão confiança em iniciar o processo de implementação, validação, e o mais importante, ter a documentação toda documentada.
Mais a frente neste artigo você vai ter contato com arquivos AsyncAPI já documentados. A especificação do padrão busca definir e anotar os diferentes componentes envolvidos dentro de uma arquitetura orientada a eventos. Os formatos dos arquivos AsyncAPI pode ser construídos usando JSON ou Yaml. Eu vou usar o tipo Yaml neste artigo.
Documentando os componentes
Um dos grandes desafios de uma arquitetura orientada a eventos é identificar, além dos contratos das mensagens, quem são os serviços produtores de mensagens e quais são os consumidores dessa mensagens.
Na especificação AsynAPI esses componentes arquiteturais, chamados Serviços, são conhecidos como Aplicações, conforme está na citação abaixo:
An application is any kind of computer program or a group of them. It MUST be a producer, a consumer or both. An application MAY be a microservice, IoT device (sensor), mainframe process, etc. An application MAY be written in any number of different programming languages as long as they support the selected protocol. An application MUST also use a protocol supported by the server in order to connect and exchange messages*.
Agora é hora de por a mão na massa 😃
Documentando o serviço de Reservas 🏗️
Documentação do serviço de Reservas:
asyncapi: 3.0.0
info:
title: Serviço de Reservas
version: 1.0.0
description: |-
A API de reservas permite realizar e gerenciar as reservas dos quartos de um hotel.
### Veja as funcionalidades desse serviço:
* Reservar um quarto 🌃
* Confirmar a reserva de um quarto 😎
* Gerir as reservas dos quartos 📈
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
defaultContentType: application/json
servers:
kafka-cluster:
host: prod.kafkacluster.org:28092
protocol: kafka
description: Cluster Kafka de produção
channels:
reservaQuartoConfirmada:
address: reservasdequartos.1.0.event.{roomId}.reservaQuarto.confirmada
messages:
reservaQuartoConfirmada:
$ref: '#/components/messages/reservaQuartoConfirmada'
description: Tópico responsável por mensagens de reservas confirmadas.
parameters:
roomId:
$ref: '#/components/parameters/roomId'
operations:
confirmarReservaQuarto:
action: receive
channel:
$ref: '#/channels/reservaQuartoConfirmada'
summary: >-
Ação utilizada para realizar a confirmação da reserva de um quarto.
traits:
- $ref: '#/components/operationTraits/kafka'
messages:
- $ref: '#/channels/reservaQuartoConfirmada/messages/reservaQuartoConfirmada'
components:
messages:
reservaQuartoConfirmada:
name: reservaQuartoConfirmada
title: Reserva do quarto confirmada
summary: >-
Inform about environmental lighting conditions of a particular
streetlight.
contentType: application/json
payload:
$ref: '#/components/schemas/reservaQuartoConfirmada'
schemas:
reservaQuartoConfirmada:
type: object
properties:
quartoId:
type: string
entrada:
type: string
format: date-time
saida:
type: string
format: date-time
nomeHospede:
type: string
enviadaEm:
$ref: '#/components/schemas/enviadoEm'
enviadoEm:
type: string
format: date-time
description: Date and time when the message was sent.
parameters:
roomId:
description: Identificador do quarto.
operationTraits:
kafka:
bindings:
kafka:
clientId:
type: string
enum:
- app-reservas
A primeira linha da especificação começa com o tipo de documento, asyncapi, e a versão (3.0.0). Esta linha não precisa ser a primeira, mas é uma prática recomendada.
O objeto info contém as informações mínimas necessárias sobre o aplicativo. Ele contém o título, que é um nome da aplicação (serviço) e sua versão. Embora não seja obrigatório, é altamente recomendável alterar a versão sempre que fizer alterações no serviço.
Seção dedicada a especificar os servidores
A solução apresentada funciona com base em brokers de mensagens, ou servidores de mensagens. Isso obriga que durante a documentação, você identifique e documente qual ou quais servidores de mensagens estão sendo usados pela aplicação (serviço).
servers:
kafka-cluster:
host: prod.kafkacluster.org:28092
protocol: kafka
description: Cluster Kafka de produção
Usando essa seção do documento, você pode documentar outros servidores de mensagens. Mais detalhes veja na documentação oficial.
Documentando Channels, operations e messages
Fazendo uma tradução direta sobre os termos do título acima, os channels são tópicos ou filas, onde serão armazenadas as mensagens publicadas. As operations são as operações que a aplicação (serviço) vai executar dentro do seu próprio contexto, podendo ser de envio (SEND) ou de recebimento (RECEIVE). Dessa forma, a especificação dá visibilidade sobre onde e como a aplicação vai se conectar e publicar o consumir mensagens.
channels:
reservaQuartoConfirmada:
address: reservasdequartos.1.0.event.{roomId}.reservaQuarto.confirmada
messages:
reservaQuartoConfirmada:
$ref: '#/components/messages/reservaQuartoConfirmada'
description: Tópico responsável por mensagens de reservas confirmadas.
parameters:
roomId:
$ref: '#/components/parameters/roomId'
operations:
confirmarReservaQuarto:
action: send
channel:
$ref: '#/channels/reservaQuartoConfirmada'
summary: >-
Ação utilizada para realizar a confirmação da reserva de um quarto.
traits:
- $ref: '#/components/operationTraits/kafka'
messages:
- $ref: '#/channels/reservaQuartoConfirmada/messages/reservaQuartoConfirmada'
Cada operação é mapeada para um tópico ou fila, informando qual é a ação específica que vai ser executada SEND ou RECEIVE, além de especificar qual é o schema da mensagem que será ou enviada ou recebida desse tópico ou fila.
O contrato da mensagem é o que estamos buscando documentar, afim de dar visibilidade sobre os dados que estão trafegando de um serviço para o outro, dentro de uma arquitetura distribuída.
Documentando mensagens e o payload
No diagrama da solução há serviços que apenas publica mensagens (eventos) e outros que publicam e consomem mensagens de eventos.
components:
messages:
reservaQuartoConfirmada:
name: reservaQuartoConfirmada
title: Reserva do quarto confirmada
summary: >-
Inform about environmental lighting conditions of a particular
streetlight.
contentType: application/json
payload:
$ref: '#/components/schemas/reservaQuartoConfirmada'
schemas:
reservaQuartoConfirmada:
type: object
properties:
quartoId:
type: string
entrada:
type: string
format: date-time
saida:
type: string
format: date-time
nomeHospede:
type: string
enviadaEm:
$ref: '#/components/schemas/enviadoEm'
O trecho documentado acima, você encontra a documentação do payload, ou seja, do schema (estrutura) da mensagem de evento que será tanto publicada quanto consumida em um dado momento do tempo. As definições do esquema de uma mensagem usando AsyncAPI, são 100% compatíveis com o esquema do tipo JSON.
Documentando o serviço Administração Hotel
A documentação do serviço de administração de hotel será similar a documentação do serviço anterior, isso porque a mensagem que é publicada pelo serviço de reservas será a mesma mensagem consumida pelo serviço de administração.
asyncapi: 3.0.0
info:
title: Serviço de administração do hotel
version: 1.0.0
description: |-
A API de administração do hotel permite realizar e gerir todos os dados relativos ao hotel e os respectivos quartos.
### Veja as funcionalidades desse serviço:
* Registrar quartos 🛏️
* Gerir dados do hotel 📇
* Gerir dados dos quartos 📈
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
defaultContentType: application/json
servers:
kafka-cluster:
host: prod.kafkacluster.org:28092
protocol: kafka
description: Cluster Kafka de produção
channels:
reservaQuartoConfirmada:
address: reservasdequartos.1.0.event.{roomId}.reservaQuarto.confirmada
messages:
reservaQuartoConfirmada:
$ref: '#/components/messages/reservaQuartoConfirmada'
description: Tópico responsável por mensagens de reservas confirmadas.
parameters:
roomId:
$ref: '#/components/parameters/roomId'
operations:
confirmarReservaQuarto:
action: receive
channel:
$ref: '#/channels/reservaQuartoConfirmada'
summary: >-
Ação utilizada para realizar a confirmação da reserva de um quarto.
traits:
- $ref: '#/components/operationTraits/kafka'
messages:
- $ref: '#/channels/reservaQuartoConfirmada/messages/reservaQuartoConfirmada'
components:
messages:
reservaQuartoConfirmada:
name: reservaQuartoConfirmada
title: Reserva do quarto confirmada
summary: >-
Inform about environmental lighting conditions of a particular
streetlight.
contentType: application/json
payload:
$ref: '#/components/schemas/reservaQuartoConfirmada'
schemas:
reservaQuartoConfirmada:
type: object
properties:
quartoId:
type: string
entrada:
type: string
format: date-time
saida:
type: string
format: date-time
nomeHospede:
type: string
enviadaEm:
$ref: '#/components/schemas/enviadoEm'
enviadoEm:
type: string
format: date-time
description: Date and time when the message was sent.
parameters:
roomId:
description: Identificador do quarto.
operationTraits:
kafka:
bindings:
kafka:
clientId:
type: string
enum:
- app-administracao-hotel
Através dessas duas documentações, somos possíveis de descobrir quem são os produtores, consumidores e principalmente quais são os dados que ambos os serviços estão consumindo.
A imagem abaixo ilustra o resultado da documentação usando AsyncApi Studio:
💡Se você quiser se divertir um pouco, pode acessar o AsyncAPI Studio.
Principais conclusões
Adotar o padrão de especificação AsyncAPI em sistemas que aplicação uma arquitetura orientada a eventos (EDA - Event-Driven Architecture), aumenta a consistência, eficiência e a governança entre diferentes times de desenvolvimento.
O ecossistema de ferramentas do AsyncAPI ajuda a acelerar o desenvolvimento de aplicativos, automatizando tarefas tediosas, mas necessárias, como geração de código, geração de documentação, validadores, etc.
A comunidade AsyncAPI está crescendo muito. Sua contribuição para a comunidade será valiosa em termos de criação de melhores aplicativos orientados a eventos.
Referências
Posted on May 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.