Lemes Workspublic engineering archive
LW / PROJETO / ecofy

EcoFy — plataforma financeira event-driven

Sete microsserviços Spring Boot atrás de um API Gateway, com Kafka, Transactional Outbox e JWT/JWKS com rotação de chave — e um SPA React 19 de arquitetura hexagonal que roda com ou sem backend trocando uma variável de ambiente.

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.

evidence: diagramsource: mermaid
lang: mermaid
Componentes implementados e fluxo de eventos

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_MANDATORY no 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.
  • ErrorHandlingDeserializer no consumidor. Um JSON malformado vira erro tratável em vez de poison pill travando o poll() 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.

ModoFonte de dadosUso
Mock ModelocalStorage, sem redeDemo pública e desenvolvimento sem backend
API ModeGateway real via proxy do ViteIntegraçã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:

stack completa
cd ecofy-backend
docker compose up -d --build
docker compose ps

O frontend roda sem nenhuma dependência de backend:

frontend em mock mode
cd ecofy-frontend
npm install
npm run dev

A primeira subida do backend compila tudo dentro das imagens e leva alguns minutos. O frontend sobe em segundos e já entra em Mock Mode.

EcoFy — plataforma financeira event-driven | Lemes Engineering