Primeiro post de uma série sobre arquitetura orientada a eventos (EDA),
construída em público a partir de um case fictício: a Faculdade Leal. Este
post fica em nível de arquitetura — a implementação (Apache Camel, código
dos consumers, etc.) vem nos posts seguintes, conforme for sendo construída.
O problema
Imagine que você é responsável pelo financeiro de uma faculdade grande.
Todo mês, o sistema precisa gerar a fatura de milhares de alunos — calculando
matérias cursadas, dependências e taxas de serviço — e hoje isso leva cerca
de 4 dias.
Paralelamente, quando um aluno entra no site pra pegar o boleto, o sistema
aciona outra procedure (GeraFaturaFinal), que lê o resultado já
calculado — guardado na TbFatura — e, na mesma execução síncrona,
chama duas APIs externas em sequência: primeiro o Sistema de Pagamentos,
pra gerar o boleto; depois a Prefeitura, pra emitir a nota fiscal (NFS-e).
Se a segunda falhar, o processo inteiro retorna erro — mesmo o boleto que
já tinha sido gerado é descartado. Funciona bem com pouco acesso e os dois
terceiros no ar. Perto do vencimento, com muito acesso simultâneo e qualquer
instabilidade da Prefeitura, o site fica lento, cai, ou devolve erro em
pedidos que já tinham, na prática, dado certo.
A arquitetura atual está desta forma:
Fluxo em lote — cálculo da fatura
Fluxo sob demanda — boleto no clique do usuário
(desenho original: https://github.com/lealnetosena/faculdade-leal/blob/master/diagramas/arquitetura-atual.drawio; versão em
Mermaid equivalente em https://github.com/lealnetosena/faculdade-leal/blob/master/diagramas/01-arquitetura-atual.md)
Os problemas, resumidos:
- As tabelas
TbLancamentoseTbFaturatêm dados acumulados de anos, sem processo de expurgo ou limpeza, e faltam alguns índices. - O processamento é feito linha a linha, aluno por aluno, e não em lote.
- O usuário pede a fatura e ela executa diversas chamadas síncronas, tanto para o sistema legado quanto para as APIs externas.
Soluções propostas e nova arquitetura
Três frentes resolvem os problemas acima. As duas primeiras são ajustes
pontuais no banco de dados; a terceira é a decisão arquitetural central
deste post, e é ela que o resto do texto detalha:
- Criação de índices.
- Criação de processo de expurgo no banco de dados.
- Vamos implantar arquitetura orientada a eventos (EDA): cada etapa — cálculo da fatura, geração do boleto, emissão da nota fiscal — passa a reagir a um evento, em vez de depender de uma chamada síncrona encadeada. Isso desacopla as etapas entre si e do legado.
A implantação consiste em criar um broker de mensageria (Kafka) com eventos
bem definidos.
1. Producer — fatura pronta
O legado não muda nada — continua realizando o processo de cálculo do jeito
que já faz. Quem avisa que uma fatura ficou pronta é um componente novo, e
aqui existem duas soluções possíveis, porque nem todo ambiente vai
permitir a ideal.
Solução A — Polling (mais simples, acesso padrão)
Um projeto lê a TbFatura a cada 30 minutos e publica as faturas prontas no
evento fatura.pronta. Não exige nenhuma permissão especial no banco — só
uma conexão de leitura comum, do tipo que qualquer aplicação já tem. Em
troca, aceita até 30 minutos de atraso e uma consulta periódica na tabela.
Solução B — CDC / Debezium (ideal, mas exige acesso de replicação)
Um conector lê o log de transação do banco diretamente — o registro
interno que todo banco já mantém, pra recuperação e réplica — e reage em
milissegundos, sem tocar na tabela nem no legado. O problema: isso exige que
o time de banco de dados libere uma permissão especial (usuário de
replicação), e nem toda empresa concede isso com facilidade, ou já tem a
infraestrutura (Kafka Connect) pronta.
Por enquanto, seguimos com a Solução A, documentando a B como evolução
possível se o acesso existir ou se o atraso de 30 min virar uma dor real.
2. Consumer — finaliza fatura
Em vez de finalizar a fatura no momento em que o usuário clica, isso passa a
acontecer antes: como o processo de geração já demora até 4 dias, durante
essa janela, conforme as faturas vão ficando prontas (fatura.pronta), já
rodamos o cálculo final que hoje é feito no clique (o passo 1.2 de
GeraFaturaFinal). Ao terminar, publica fatura.finalizada — só isso, sem
gravar nada. Persistir é responsabilidade de outro componente.
3. Consumer — persiste fatura (atômico)
Um componente com uma responsabilidade só: ouvir fatura.finalizada e
gravar o resultado num banco de dados próprio (não o do legado — um
armazenamento novo, dono desse dado). A gravação é insert-only: cada
cálculo vira uma linha nova, com data e hora, nunca sobrescrevendo a
anterior. Isso dá dois resultados de graça — uma consulta rápida (a linha
mais recente de um aluno) e um histórico completo, útil pra auditoria,
inclusive se um valor foi recalculado depois, e quando. É esse banco que o
Consumer gerar boleto vai consultar mais tarde, no clique do usuário.
4. Consumer — gerar boleto
Também usa fila, mas dispara no clique do usuário (boleto.solicitado),
não de forma proativa: gerar o boleto com antecedência pode ficar errado se
o aluno pagar via PIX, que não aplica o mesmo desconto. O PIX em si não é o
problema hoje, está funcional — mas ainda assim ele isola o boleto do
fornecedor de pagamento usado: a mensagem carrega um campo dizendo qual é o
fornecedor, e mantém o design plugável pra escalar com mais projetos
conectados. (PIX fica fora do escopo do resto da série — citado aqui só como
o motivo de design por trás do boleto ser gerado no clique, não de forma
proativa.) Ao final, publica boleto.gerado, que aciona o Consumer de
Nota Fiscal e também devolve o boleto pro usuário.
5. Consumer — Nota Fiscal
Verifica o evento boleto.gerado e aciona a API da Prefeitura. Esta é a
etapa mais problemática hoje, então aqui entra retry em caso de erro, com
uma fila própria (notafiscal.erro) pra tratar o que esgotar as tentativas.
Em caso de sucesso, publica notafiscal.pronta.
6. Consumer — nota fiscal pronta
Conectado à API de notificação e ao site — o site terá um local pra
consultar a nota, e ela também será enviada por e-mail ao usuário.
Por que isso escala naturalmente
Com esses componentes isolados, temos possibilidade de escalar
horizontalmente, e isso vai acontecer naturalmente, por dois motivos:
O sistema legado tem uma pendência de expurgo e manutenção de índice e
performance (existe até oportunidade de melhorar a procedure). Como é
algo crítico e exige bastante validação, isso vai acontecer ao longo dos
meses. Conforme a velocidade do processamento das faturas for melhorando,
a capacidade dos consumers pode ser expandida — e isso pode até ser feito
de forma automática, usando Kubernetes.Na aquisição de uma nova unidade, ela também vai precisar inserir no
consumer de fatura pronta (ou de fatura final), e, caso tenha um volume
maior, os consumers também podem crescer automaticamente.
Eventos — quem publica, quem consome
| Evento | Publica | Consome | Quando |
|---|---|---|---|
fatura.pronta |
Producer (polling 30min) | Consumer finaliza fatura |
CalculaFatura terminou pra 1 aluno |
fatura.finalizada |
Consumer finaliza fatura | Consumer persiste fatura — grava insert-only num banco próprio (não o do legado) | Cálculo final rodou, proativo |
boleto.solicitado |
Consumer gerar boleto (dispara com o clique do usuário no Site) | Consumer gerar boleto — consulta o banco (não consome fatura.finalizada de novo) |
Aluno clicou pedindo o boleto |
boleto.gerado |
Consumer gerar boleto | Consumer Nota Fiscal (e o próprio Consumer gerar boleto usa a resposta pra devolver o boleto ao usuário) | Sistema de Pagamentos respondeu |
notafiscal.pronta |
Consumer Nota Fiscal | Consumer nota fiscal pronta (site/e-mail) | Prefeitura respondeu |
notafiscal.erro |
Consumer Nota Fiscal | Fila de erro / alerta | Esgotou retry |
A nova arquitetura
(diagramas: ver https://github.com/lealnetosena/faculdade-leal/blob/master/diagramas/02-arquitetura-proposta.md — a arquitetura
completa com os componentes descritos acima, além do detalhamento
completo com os nomes concretos de tecnologia em
https://github.com/lealnetosena/faculdade-leal/blob/master/diagramas/arquitetura-atual.drawio)
flowchart LR
Legado["Sistema Acadêmico<br/>(legado, sem mudança)"] --> Producer["Producer<br/>fatura pronta"]
Producer -->|fatura.pronta| CF["Consumer<br/>finaliza fatura"]
CF -->|fatura.finalizada| CP["Consumer<br/>persiste fatura<br/>(atômico)"]
CP --> Banco[("Banco próprio<br/>insert-only")]
Aluno(["Aluno<br/>clica no site"]) -->|boleto.solicitado| CB["Consumer<br/>gerar boleto"]
Banco --> CB
CB --> Pagamentos["Sistema de<br/>Pagamentos"]
CB -->|boleto entregue| Recebido(["Aluno recebe<br/>o boleto"])
CB -->|boleto.gerado| CNF["Consumer<br/>Nota Fiscal"]
CNF --> Prefeitura["Sistema Prefeitura<br/>NFS-e"]
CNF -->|notafiscal.erro| ErroFila[("Fila de erro")]
CNF -->|notafiscal.pronta| CNP["Consumer<br/>nota fiscal pronta"]
CNP --> Aviso(["Aluno é avisado<br/>site / e-mail"])
classDef novo fill:#d5e8d4,stroke:#82b366
class Producer,CF,CP,CB,CNF,CNP novo
Versão detalhada, com os nomes concretos de tecnologia (Spring Batch, Spring
Boot, Apache Camel) e a numeração passo a passo — desenhada no draw.io:
Fluxo de cálculo — fatura pronta e persistida
Fluxo de boleto — request-reply sobre Kafka + nota fiscal isolada
Bala de prata?
A arquitetura orientada a eventos (EDA) é uma ferramenta importante para
resolver problemas complexos e grandes. Este cenário hipotético é bem
simplificado, mas dá um gosto de um contexto maior, expandido com diversas
áreas. E "bala de prata" é bem a palavra errada pra usar aqui — como toda
decisão de arquitetura, ela troca um conjunto de problemas por outro. Abaixo,
os prós e os contras de implantar essa arquitetura, de forma mais técnica:
Prós
- Desacoplamento entre times e sistemas — quem publica e quem consome não precisam se conhecer, nem fazer deploy juntos, nem compartilhar banco.
- Isolamento de falha — se um consumer cai ou fica lento, não derruba quem publicou nem os outros consumers.
- Escala independente — aumenta instâncias de um consumer sem tocar no publicador, e vice-versa.
- Um fato, várias reações (fan-out) — quando algo precisa disparar N ações independentes, o publicador não precisa saber nem orquestrar quem são elas.
- Desacoplamento no tempo — publicador e consumidor não precisam estar no ar ao mesmo tempo.
- Trilha de auditoria e replay — com um log como o Kafka, existe histórico do que aconteceu, e dá pra reprocessar.
- Ponte entre legado e novo sem reescrever o legado — o sistema antigo só publica um evento, o resto reage sem tocar nele (Strangler Fig).
Contras
- Complexidade operacional — de um sistema só, passamos a ter 6+ processos independentes, cada um com seu próprio deploy, log e monitoramento.
- Consistência eventual, por design — o aluno pode ver o boleto pronto e a nota ainda "em processamento"; o site precisa saber lidar com estados intermediários, algo que não existia no fluxo síncrono de hoje.
- Falha parcial vira um estado real — sem retry e fila de erro bem pensados, uma mensagem perdida é pior do que o erro síncrono de hoje, porque ninguém percebe na hora.
- Depuração mais difícil — em vez de olhar um log só, é preciso correlacionar eventos entre vários serviços pra entender o que aconteceu com uma fatura específica.
- Curva de aprendizado — o time precisa entender conceitos que não existiam no mundo síncrono: mensageria, consumer groups, idempotência.
- Atraso da Solução A — o polling ainda deixa até 30 minutos de defasagem; não é tempo real de verdade até evoluir pra CDC.
Eu me considero um eterno estudante. Esse artigo foi criado por mim, com
auxílio de inteligência artificial. Caso encontre algum erro ou alguma
melhoria, fique à vontade para me enviar.




Top comments (0)
Some comments have been hidden by the post's author - find out more