O problema
Dados financeiros brutos chegam desestruturados: extratos em CSV e OFX, com codificações inconsistentes, descrições truncadas e nenhuma semântica. Transformar isso em orçamento, insight e alerta exige um pipeline que sobreviva a arquivo malformado, a serviço fora do ar e a reprocessamento.
A restrição que definiu a arquitetura: a importação não pode bloquear o usuário. Subir um extrato de mil linhas e esperar a categorização terminar é inaceitável — mas a resposta imediata só é honesta se o processamento assíncrono for confiável de verdade.
A decisão de arquitetura
Sistema distribuído orientado a eventos: sete microsserviços em Spring Boot atrás de um API Gateway único, com Kafka como barramento central e um banco por serviço.
Três decisões carregam o custo dessa escolha:
Segurança federada, sem sessão. O ms-auth emite JWT RS256; cada serviço valida sozinho via jwk-set-uri, consumindo o JWKS. Não há estado compartilhado de autenticação, e a rotação de chave acontece sem derrubar ninguém.
Ponto de entrada único. O frontend conhece apenas o gateway. O cliente HTTP recusa URLs absolutas e rejeita apontar direto para as portas 8081–8087 — a regra é aplicada em código, não em convenção.
Banco por serviço. PostgreSQL isolado por microsserviço, MongoDB no ms-notification. Sem join entre domínios; a consistência atravessa por evento.
O backend: decisões defensivas
O que diferencia o projeto não é a lista de funcionalidades, e sim as decisões que só aparecem quando alguém pensou no modo de falha:
PROPAGATION_MANDATORYno Outbox. Torna impossível gravar na tabela de outbox fora da transação do domínio. O padrão Transactional Outbox só funciona se essa garantia for estrutural, não disciplinar.ErrorHandlingDeserializerno consumidor. Um JSON malformado vira erro tratável em vez de poison pill travando opoll()e parando a partição inteira.- Decodificação estrita de charset. Um extrato em Latin-1 falha de forma barulhenta em vez de corromper silenciosamente a descrição de uma transação financeira.
- Health indicator do Outbox fora do liveness probe. Reiniciar o pod não conserta o broker — essa checagem no liveness só produziria um loop de restart.
- Idempotência arbitrada por constraint de banco. Quem decide se o evento já foi processado é o índice único, não uma verificação em memória sujeita a corrida.
O frontend: inversão de dependência no cliente
O SPA em React 19 + TypeScript + Vite não é uma casca sobre a API. Ele aplica arquitetura hexagonal do lado do cliente: a UI depende de portas, não de HTTP.
A camada services/ concentra as abstrações — data-source, http, session, errors, money, pagination, config — e o resolve-data-source decide em runtime qual adaptador atende cada feature. As dez features (auth, budgets, categories, dashboard, goals, imports, insights, notifications, users, foundation) consomem essa porta sem saber quem está do outro lado.
O resultado prático: a mesma UI roda em dois modos, sem alterar uma linha de componente.
| Modo | Fonte de dados | Uso |
|---|---|---|
| Mock Mode | localStorage, sem rede | Demo pública e desenvolvimento sem backend |
| API Mode | Gateway real via proxy do Vite | Integração com os sete microsserviços |
O frontend mantém ainda um design system próprio e documenta os contratos de cada serviço em docs/ (AUTH_CONTRACTS.md, BUDGETING_CONTRACTS.md, INGESTION_CONTRACTS.md e os demais), o que mantém os enums alinhados entre front e back.
O que ficou em aberto
Autorização com cobertura incompleta. O padrão de ownership existe e está correto no ms-users, ms-budgeting e ms-ingestion — o userId vem da claim do JWT e o valor enviado pelo cliente é ignorado. Mas o ms-insights e o ms-notification não fazem verificação de identidade: os endpoints devolvem dados de qualquer usuário. A decisão certa já foi tomada; falta aplicá-la em dois serviços.
Chave privada no histórico do git. O arquivo saiu da working tree, mas permanece nos commits anteriores. Deve ser tratada como comprometida e rotacionada — remover do HEAD não remove do histórico.
Confiabilidade. O ms-users não tem DLT, então mensagem malformada é reentregue indefinidamente. Não há tracing distribuído: o correlation ID liga os logs, mas não dá latência por span. Não há ferramenta de replay de DLT — recuperação é manual no broker.
Inconsistências de contrato. Paginação diverge entre serviços (PageResponse / PagedResponse / limit puro) e o ms-budgeting devolve o código de erro em details.code em vez do errorCode de topo.
Como rodar
A stack inteira sobe com Docker Compose — infraestrutura, sete serviços e gateway:
cd ecofy-backend
docker compose up -d --build
docker compose psO frontend roda sem nenhuma dependência de backend:
cd ecofy-frontend
npm install
npm run devA primeira subida do backend compila tudo dentro das imagens e leva alguns minutos. O frontend sobe em segundos e já entra em Mock Mode.