Eu desenvolvi este projeto como uma implementação de um núcleo de transações financeiras (Wallet Core) baseada em Arquitetura Orientada a Eventos (EDA).
Ele nasceu como parte do desafio do curso Full Cycle, mas eu decidi utilizar uma stack moderna e funcional com Clojure e Polylith, focando em imutabilidade, modularidade e separation of concerns.
Eu estruturei o projeto como um Monorepo modular gerenciado pelo Polylith:
- Linguagem: Clojure (JDK 17+)
- Gerenciamento de Workspace: Polylith (
poly) - Validação de Dados: Malli
- Banco de Dados: PostgreSQL 15 (via
next.jdbceHoneySQL) - Mensageria: Apache Kafka (via
Jackdaw) - API: Jetty + Reitit + Ring
bases/
└── wallet-api/ # API Gateway (REST -> Componentes)
└── balances-api/ # API Balance (REST + Kafka Consumer) [NOVO]
└── event-processor/ # Worker (Processamento Assíncrono)
components/
├── account/ # Domínio de Contas e Saldo
├── balance/ # Domínio de Leitura de Saldos (CQRS Projection) [NOVO]
├── client/ # Domínio de Clientes
├── transaction/ # Core: Atomicidade e Orquestração
├── database/ # Infra: Connection Pool e Migrations
└── kafka/ # Infra: Producers
projects/
└── wallet/ # Artefato Deployável (Wallet Core)
└── balances/ # Artefato Deployável (Balances Service) [NOVO]
Este projeto foi desenvolvido não apenas como uma resposta aos diffs do desafio, mas como um sistema completo e resiliente que garante a integridade dos dados através de uma arquitetura orientada a eventos.
| Requisito | Implementação e Garantia de Integridade |
|---|---|
| Microsserviço Independente | O serviço de Balances é um projeto Polylith isolado, com seu próprio ciclo de vida, banco de dados e porta (3003), garantindo o desacoplamento físico e lógico. |
| Consistência Eventual | A integridade entre o Wallet Core e o Balances é mantida via Kafka. O fluxo garante que qualquer alteração de saldo no Core seja propagada de forma assíncrona e confiável. |
| Persistência de Balances | Diferente de uma simples cache, o Balances possui seu próprio banco PostgreSQL (balances-db), permitindo consultas históricas e recuperação de estado independentemente da Wallet. |
| Automação Total (Seed & Migrations) | O sistema é "zero touch". Ao subir o Docker, as migrations do Wallet e do Balances rodam em paralelo, e o Wallet popula dados iniciais (Seed) que fluem automaticamente até o Balances via eventos. |
| Contrato de Dados | A estrutura de mensagens no Kafka segue um padrão estrito, garantindo que o consumidor de Balances processe apenas informações válidas para atualização de saldo. |
| Documentação Viva | O arquivo requests.http foi estendido para incluir testes de ponta a ponta que validam a integridade do fluxo desde a transação até a consulta no novo microsserviço. |
- Wallet Core executa uma transação ACID no Postgres.
- Um evento
BalanceUpdatedé emitido com o estado final do saldo. - O Balances Service consome o evento e realiza um
upsertatômico no seu banco. - O usuário consulta
GET /balances/{id}e recebe o dado projetado e persistido especificamente para leitura.
- Docker & Docker Compose
- Clojure CLI
- Ferramenta
poly(opcional, mas recomendada)
Para rodar todos os microsserviços (Wallet Core + Balances + Infra) de uma vez:
docker compose up -dO que acontece automaticamente:
- Os containers sobem (Postgres, Kafka, Zookeeper, Wallet, Balances).
- O Wallet Core detecta que o banco está vazio e roda o Seed Automático.
- Cria Clientes e Contas.
- Faz transações que geram eventos.
- O Balances Service recebe os eventos e atualiza seu próprio banco.
Se você quiser rodar um dos serviços via terminal (REPL):
Wallet Core:
cd projects/wallet
clojure -M -m br.com.eda.wallet-api.coreBalances Service:
cd projects/balances
clojure -M -m br.com.eda.balances-api.coreA API do Wallet estará disponível em: http://localhost:8080
A API do Balances estará disponível em: http://localhost:3003
Para facilitar os testes, eu preparei o arquivo requests.http (requer extensão REST Client), mas você também pode utilizar os comandos curl abaixo.
Verifica se a API está online.
curl -X GET http://localhost:8080/healthcurl -X POST http://localhost:8080/clients \
-H "Content-Type: application/json" \
-d '{"name": "Neo", "email": "neo@matrix.com"}'curl -X GET http://localhost:8080/clientsUse o id retornado na criação do cliente.
curl -X POST http://localhost:8080/accounts \
-H "Content-Type: application/json" \
-d '{"client_id": "UUID_DO_CLIENTE"}'curl -X GET http://localhost:8080/accountscurl -X GET http://localhost:8080/accounts/UUID_DA_CONTA/balanceIsso debita da origem, credita no destino (Atomicamente) e publica no Kafka.
curl -X POST http://localhost:8080/transactions \
-H "Content-Type: application/json" \
-d '{
"account_id_from": "UUID_CONTA_ORIGEM",
"account_id_to": "UUID_CONTA_DESTINO",
"amount": 100
}'curl -X GET http://localhost:8080/accounts/UUID_DA_CONTA/transactionsEste endpoint consulta o banco de dados exclusivo do serviço de Balances.
curl -X GET http://localhost:3003/balances/UUID_DA_CONTA
# Exemplo de resposta: {"account_id":"...","balance":100,"updated_at":"..."}Para rodar os testes de todos os componentes:
clojure -M:poly test
Para verificar a integridade do workspace:
clojure -M:poly check
Na minha implementação atual do componente Transaction, eu utilizei uma abordagem pragmática para o escopo deste exercício:
- Commit da transação no PostgreSQL (Atomicidade garantida via
jdbc/with-transaction). - Publicação do evento no Kafka (Fire and forget).
Cenário de Risco: Existe uma janela de falha teórica (milissegundos) entre o commit do banco e a publicação no Kafka. Se o processo da aplicação for encerrado abruptamente (Crash/OOM/Falha de Rede) exatamente neste intervalo, o sistema entrará em estado inconsistente (Dinheiro debitado, mas evento não emitido).
Solução para Produção: Para evoluir este projeto para um ambiente crítico, a solução recomendada seria implementar o Transactional Outbox Pattern:
- Persistir o evento em uma tabela
outboxdentro da mesma transação SQL da transferência. - Utilizar um processo assíncrono (Relay ou CDC com Debezium) para ler a tabela
outboxe publicar no Kafka com garantia de entrega At-Least-Once.
- Idempotência no Consumo: Garantir que os consumidores Kafka lidem com mensagens duplicadas.
- Schema Registry: Adotar Avro ou JSON Schema para contrato estrito de mensagens.
- Distributed Tracing: Implementar OpenTelemetry para rastrear o fluxo entre API -> DB -> Kafka.
Neste diagrama, eu mostro exatamente o que acontece desde o momento em que o usuário chama a API até o Worker processar o evento.
sequenceDiagram
autonumber
participant U as Usuário (HTTP)
participant API as Wallet API (App)
participant DB as Postgres (Wallet)
participant K as Kafka
participant B as Balances Service
participant DB2 as Postgres (Balances)
Note over U, API: 1. Início da Requisição
U->>API: POST /transactions
activate API
Note over API, DB: 2. Unit of Work (Atômico)
API->>DB: BEGIN TRANSACTION
activate DB
API->>DB: Debita Conta Origem
API->>DB: Credita Conta Destino
API->>DB: COMMIT
deactivate DB
Note over API, K: 3. Notificação (Fire & Forget)
API->>K: Produce "BalanceUpdated"
API-->>U: 201 Created (Transação Concluída)
deactivate API
Note over K, B: 4. Processamento Assíncrono (Event Driven)
loop Polling
B->>K: Poll (Novas mensagens?)
K-->>B: Evento: {Payload: ...}
end
activate B
Note over B, DB2: 5. Atualização de Leitura
B->>DB2: Upsert Balance (Novo Saldo)
deactivate B
Aqui eu explico o que acontece em cada fase numerada no diagrama:
O cliente (pode ser um App Mobile, Frontend ou Postman) envia o JSON pedindo a transferência.
- Responsável:
bases/wallet-api(Handlers). - Validação: O
Malliverifica se os IDs são strings e se o valor é positivo.
É aqui que a mágica acontece. Eu usei jdbc/with-transaction para garantir a integridade financeira.
- Onde:
components/transaction. - Ação: O dinheiro sai de A e vai para B matematicamente.
- Garantia: Se a luz acabar na linha do "Débito", o "Crédito" nunca acontece e o banco faz Rollback automático. O dinheiro está seguro.
Imediatamente após o banco confirmar "OK, gravei", a API avisa o Kafka.
Em um container separado, o Balances Service acorda ao receber o evento.
- Onde:
bases/balances-api(Consumer). - Ação: Ele deserializa o evento
BalanceUpdated. - Efeito: Ele atualiza a tabela
balancesno banco dedicado. Isso permite que o saldo seja consultado rapidamente sem sobrecarregar o banco principal de transações.
Se o passo 4 falhar (ex: serviço de Balances fora do ar), a transação não é cancelada. O dinheiro já foi movido no passo 2. O Consumers apenas reprocessa o evento quando o serviço voltar. Isso garante Alta Disponibilidade para a Wallet.
Para transformar meu projeto acadêmico eda-study em uma arquitetura de Fintech Real ("Battle-Tested"), eu precisaria endereçar problemas que só aparecem em escala e com dinheiro de verdade em jogo: Fraude, Consistência Eventual, Latência e Escalabilidade de Leitura.
Abaixo, eu ilustro como esse sistema evoluiria na vida real.
Neste cenário, o fluxo não é apenas "Mover dinheiro e Logar". O evento TransactionCreated dispara uma reação em cadeia em vários departamentos (Microsserviços).
graph TD
User[📱 App do Usuário]
API[🛡️ Wallet API Gateway]
subgraph "Core Transacional (Write Side)"
DB_Write[(🐘 Postgres Master)]
Outbox[📦 Tabela Outbox]
end
subgraph "Event Bus"
Kafka{Apache Kafka}
Debezium[CDC Connector]
end
subgraph "Consumers (Workers Especializados)"
Fraud[🕵️ Anti-Fraud Service]
Ledger[📒 Ledger Imutável]
Notif[🔔 Notification Service]
Projector[📽️ Read Model Projector]
end
subgraph "Leitura Otimizada (Read Side)"
DB_Read[(🍃 MongoDB / Elastic)]
end
%% Fluxo
User -->|"1. Idempotency-Key + Payload"| API
API -->|"2. Validação + Rate Limit"| DB_Write
DB_Write -->|"3. Commit (Transação + Evento Outbox)"| Outbox
Outbox -.->|"4. CDC (Leitura de Log)"| Debezium
Debezium -->|"5. Publica Evento"| Kafka
Kafka -->|"6a. Analisa Risco"| Fraud
Kafka -->|"6b. Auditoria Contábil"| Ledger
Kafka -->|"6c. Push/Email"| Notif
Kafka -->|"6d. Atualiza Visualização"| Projector
Projector -->|"7. Grava Extrato Otimizado"| DB_Read
User -.->|"8. Consulta Extrato (Rápido)"| DB_Read
Vou detalhar as camadas que eu adicionaria e por que elas são obrigatórias numa Fintech real.
No meu código atual, se o usuário clicar 2 vezes no botão "Transferir", o dinheiro sai 2 vezes.
- Realidade: O App envia um Header
Idempotency-Key(um UUID gerado no celular). - Engenharia: A API verifica no Redis se essa chave já foi processada. Se sim, retorna o resultado anterior cacheado, sem tocar no Postgres. Isso evita Double Spending em redes instáveis.
Como eu mencionei antes, o código atual tem o risco de escrever no banco e falhar no Kafka.
- Realidade: Usamos CDC (Change Data Capture). Ferramentas como Debezium leem o log binário do Postgres (WAL) e garantem que cada linha inserida no banco vire um evento no Kafka.
- Benefício: Zero perda de dados. O banco é a fonte da verdade, o Kafka é o reflexo fiel.
E se a transferência for aprovada pelo banco, mas o Worker de Fraude (Fraud) detectar que é uma lavagem de dinheiro 1 segundo depois? O dinheiro já saiu!
- Arquitetura: Padrão Saga (Coreografia).
- Fluxo:
- Core: Move dinheiro (Status:
PENDING). - Kafka: Publica
TransactionPending. - Fraud Worker: Analisa. Se detectar fraude, publica evento
FraudDetected. - Core (Consumer): Escuta
FraudDetectede executa uma Transação de Compensação (Estorno/Refund) e marca comoREJECTED.
No meu código atual, eu uso h/select na mesma tabela transactions que recebe escritas.
-
O Problema: Num dia de pagamento (dia 5), milhões de pessoas abrem o app para ver o saldo (Leitura), travando o banco para quem quer transferir (Escrita).
-
Realidade: Separamos leitura e escrita.
-
Write Side (Postgres): Normalizado, rígido, focado em integridade.
-
Read Side (NoSQL/Elastic): O Worker
Projectorpega o evento e monta um JSON pronto para a tela do celular ("Extrato Bonito"). -
Engenharia: O App consulta o NoSQL. É absurdamente rápido e barato, e nunca trava as transações.
A tabela transactions do meu projeto é mutável. Numa fintech, contadores precisam de um histórico que prova que o saldo de A + B é igual ao total de dinheiro no banco.
- Realidade: O Worker
Ledgergrava num banco específico (como Datomic ou XTDB em Clojure) que suporta auditoria temporal e imutabilidade criptográfica.
A stack que eu escolhi (Clojure + Kafka) é a "Arma Secreta" do Nubank e outras gigantes. Vou explicar por quê:
- Imutabilidade por Padrão: Em sistemas concorrentes (milhares de transações/segundo), não ter que lidar com objetos mudando de estado na memória evita uma classe inteira de bugs.
- EDN (Extensible Data Notation): O Clojure trafega dados como mapas. Diferente do Java (que precisa converter JSON -> Objeto -> DTO -> DAO), o Clojure processa o dado "nu e cru" dos Workers de forma extremamente performática.
- Datomic/XTDB: Bancos de dados feitos em Clojure que resolvem o problema de auditoria nativamente.
Resumo da Evolução do meu projeto: Meu projeto atual é o "MVP Funcional". A versão Fintech Real adiciona: Idempotência (Segurança), CDC (Consistência), Sagas (Reversão) e CQRS (Escalabilidade).