Sistema distribuído para monitorar entregas em tempo real: recebe pings GPS, detecta desvio de rota e atrasos, gera alertas, notifica o time de suporte e dispara recálculo de rota automaticamente. O projeto foi desenhado como um showcase de arquitetura moderna: Microservices + Event-Driven Architecture + Transactional Outbox + Polyglot Persistence + Resilience.
- Visão Geral
- Arquitetura
- Serviços
- Event-Driven Architecture (EDA)
- Eventos e Topologia de Mensageria
- Persistência (Polyglot)
- Padrões e Práticas
- Design Decisions
- Como Rodar Localmente
- Rotas Principais
- Operação (DLQ e Reprocessamento)
- Testes e Cenários
Uma empresa de logística precisa monitorar milhares de entregas em tempo real. Se um motorista:
- desviar da rota, ou
- sofrer atraso,
o sistema deve:
- gerar um alerta,
- notificar o suporte,
- e recalcular a rota (automaticamente) quando necessário.
Demonstrar domínio prático de:
- Microsserviços
- Event-Driven Architecture
- Comunicação assíncrona com broker (RabbitMQ)
- Persistência poliglota (PostgreSQL + Redis)
- Resiliência (Circuit Breaker, DLQ, idempotência)
- Tracking Service (Java / Spring Boot) Responsável por ingestão de pings GPS, detecção de rota/atraso, criação de alertas, histórico e publicação de eventos.
- Notification Service (Node.js) Consumidor de eventos de alerta; especializado em I/O (e-mail/log/push/SMS). Implementa idempotência e DLQ.
- Route Optimization Service (Laravel / PHP) Consumidor de solicitações de recálculo; calcula uma nova rota/ETA (MVP) e publica o resultado.
- Backoffice Dashboard (Next.js) Painel operacional para visualizar entregas/alertas, tratar alertas (ACK), monitorar DLQs e reprocessar mensagens.
- RabbitMQ (Message Broker)
- PostgreSQL (dados transacionais e histórico)
- Redis (cache em tempo real: última localização / cooldown / idempotência)
- Prometheus + Grafana (métricas e dashboards)
Responsabilidade:
- Receber pings GPS (
/tracking/pings) - Validar se o motorista está na rota
- Detectar alertas:
ROUTE_DEVIATIONeDELAY - Persistir histórico (Postgres)
- Atualizar cache de “última localização” (Redis)
- Publicar eventos de domínio via Outbox
Por que Spring Boot?
- Ecossistema maduro para APIs críticas
- Integração robusta com DB (JPA/Flyway), Redis, RabbitMQ, Actuator
- Ótimo suporte a observabilidade e resiliência (Micrometer, Resilience4j)
Responsabilidade:
- Consumir evento
alert.created - Validar schema do evento
- Garantir idempotência (Redis
SET NX) - Enviar notificação (log/email; extensível para SMS/push)
- Retry com backoff e DLQ para falhas definitivas
Por que Node.js?
- Excelente para workloads I/O bound
- Facilidade para integrar e expandir canais (SMTP, APIs externas)
- Bom fit para serviço “edge” de notificação
Responsabilidade:
- Consumir
route.recalc.requested - Recalcular rota/ETA (MVP) e publicar
route.recalc.completed
Por que Laravel?
- Produtividade alta e ecossistema forte
- Bom para construir regras e evoluir para algoritmos complexos
- Aproveita domínio prévio da stack
Responsabilidade:
- Listar entregas, ETA e última localização
- Ver alertas em tempo real (SSE) e tratar (ACK)
- Monitorar DLQ e reprocessar mensagens usando RabbitMQ Management API
Por que Next.js?
- UI moderna, rápida e escalável
- Integra bem com SSR/CSR conforme necessidade
- Facilita criação de “Ops pages” no mesmo frontend
Este sistema usa Event-Driven Architecture para desacoplar serviços e permitir:
- escalabilidade independente
- tolerância a falhas
- processamento assíncrono e eventual consistency
Regra principal: serviços não “chamam” outros serviços para tarefas internas (ex.: notificar ou recalcular). Em vez disso, eles publicam eventos de domínio e consumidores reagem a esses eventos.
- Tracking detecta
ROUTE_DEVIATION - Tracking grava no banco e publica
alert.created - Notification consome
alert.createde notifica suporte - Tracking também publica
route.recalc.requested(com cooldown) - Route Optimization consome e publica
route.recalc.completed - Tracking consome e atualiza rota/ETA
logistics.events
alert.createdroute.recalc.requestedroute.recalc.completed
q.notification←alert.createdq.route_opt←route.recalc.requestedq.tracking.route_updates←route.recalc.completed
q.notification.dlqq.route_opt.dlqq.tracking.route_updates.dlq
Por que DLQ?
- Separar falhas permanentes do fluxo normal
- Permitir investigação e reprocessamento
- Evitar loop infinito de requeue
deliveries: entrega, rota, destino, ETA recalculadolocation_pings: histórico de localizaçãoalerts: alertas gerados e status (ACK)outbox_events: eventos pendentes/publicadosconsumed_events: idempotência no consumo (Tracking)
Por que PostgreSQL?
- Consistência forte e transações
- Ótimo para histórico e queries operacionais
- Base sólida para Outbox e auditoria
- cache da última localização do motorista (alta frequência)
- cooldown anti-spam de recálculo
- idempotência do Notification (evita duplicar envio)
Por que Redis?
- Latência baixa e alto throughput
- Ideal para dados “hot” e chaves com TTL
Eventos são gravados na tabela outbox_events na mesma transação do domínio (alerta/ping).
Um publisher assíncrono lê a outbox e publica no broker.
Por quê? Evita inconsistências do tipo “salvou no DB mas não publicou no broker”.
- Notification: Redis
SET NXusandoeventId - Tracking consumer:
consumed_eventsimpede reaplicarroute.recalc.completed
Por quê? Mensageria normalmente é at-least-once, então duplicatas podem ocorrer.
- Circuit Breaker (Resilience4j) no publisher da outbox
- DLQ para mensagens inválidas ou falhas definitivas
- Retry com backoff no Notification
Por quê? Se a notificação cair, o Tracking não para. O sistema se mantém funcional em modo degradado.
Divisão por contexto:
- Tracking: monitoramento e regras de alerta
- Notification: canais e entrega de notificação
- Route Optimization: algoritmo de recálculo
- Backoffice: operação e visualização
Por quê? Reduz acoplamento e permite evolução independente.
Esta seção explica as principais escolhas técnicas e os trade-offs por trás do projeto.
Decisão: usar RabbitMQ para a mensageria do MVP. Por quê:
- Setup local mais simples (excelente para portfólio e execução rápida).
- Boa combinação com work queues e DLQ.
- Integração direta com Spring AMQP, Node (
amqplib) e consumidores simples no Laravel.
Trade-offs:
- Kafka é mais indicado quando se precisa de streaming de altíssimo throughput, retenção longa e replay como primeira classe.
- Para este projeto, o foco é demonstrar EDA e confiabilidade com DLQ + idempotência + outbox, o que RabbitMQ cobre muito bem.
Decisão: um exchange topic (logistics.events) com routing keys como alert.created, route.recalc.requested, etc.
Por quê:
- Facilita evolução: novos consumidores podem assinar padrões (
alert.*,route.*). - Evita “filas ponto-a-ponto” rígidas e reduz acoplamento.
Trade-offs:
- Requer disciplina de naming/versão de eventos.
- Mudanças de schema precisam ser tratadas com compatibilidade (ver decisão 9).
Decisão: o Tracking persiste eventos em outbox_events na mesma transação do domínio e publica de forma assíncrona.
Por quê:
- Garante consistência: se salvou alerta/ping, o evento não se perde.
- Evita bugs clássicos (“salvou no DB mas falhou ao publicar”).
Trade-offs:
- Introduz “eventual consistency”: o evento pode sair alguns segundos depois.
- Exige um publisher agendado (scheduler) e métricas de backlog.
Decisão: Postgres para dados transacionais/histórico e Redis para cache em tempo real e coordenação. Por quê:
- Postgres é ideal para histórico e consistência (deliveries, alerts, pings, outbox).
- Redis é ideal para “hot data” (última localização, TTL, cooldown, idempotência) com baixa latência.
Trade-offs:
- Mais componentes para operar (mas isso é parte do valor do projeto).
- Necessidade de tratar degradação: sem Redis, o sistema deve continuar (com funcionalidade reduzida).
Decisão:
- Tracking decide quando recálculo é necessário (regra de negócio).
- Route Optimization decide como recalcular (algoritmo).
Por quê:
- Mantém responsabilidades claras.
- Permite evoluir o algoritmo de rota sem tocar no Tracking.
- Demonstra separação por contexto (DDD/Bounded Contexts).
Trade-offs:
- Mais mensagens e eventual consistency entre pedir recálculo e receber resultado.
Decisão:
- Notification: idempotência por
eventIdvia Redis (SET NXcom TTL). - Tracking consumer: idempotência por
eventIdem tabelaconsumed_events.
Por quê:
- Mensageria tende a ser at-least-once. Duplicatas podem ocorrer.
- Evita notificação duplicada e reprocessamento indevido.
Trade-offs:
- Redis indisponível pode afetar a garantia do Notification (decisão 11).
- Tabela
consumed_eventscresce (mitigável com retenção/TTL por job).
Decisão: todas as filas críticas possuem DLQ e existe página /dlq no Backoffice para operação.
Por quê:
- Falhas permanentes (schema inválido, delivery inexistente, etc.) devem sair do fluxo normal.
- Operação e debug ficam simples: visualizar, corrigir causa, reprocessar.
Trade-offs:
- Reprocessar exige cuidado para não republicar “lixo” continuamente.
- Ideal ter política clara de “quando reprocessar” e auditoria (roadmap).
Decisão: aplicar Circuit Breaker (Resilience4j) para que indisponibilidade do broker não degrade o Tracking. Por quê:
- O Tracking é o serviço crítico de ingestão (não pode travar).
- Se RabbitMQ estiver instável, o sistema opera “em modo degradado” e a Outbox acumula.
Trade-offs:
- O backlog pode crescer e aumentar o tempo até notificações/recálculo.
- Necessita monitoramento (Prometheus) e alertas.
Decisão: eventos possuem envelope padrão (eventId, eventType, occurredAt, correlationId, data).
Por quê:
- Facilita rastreamento e padroniza consumidores.
- Dá base para evoluir schemas com compatibilidade.
Estratégia recomendada:
- Mudanças compatíveis: adicionar campos opcionais.
- Mudanças incompatíveis: criar novo
eventTypeou versão (alert.created.v2) e manter consumidores antigos por um período.
Decisão: usar Server-Sent Events para stream de alertas em tempo real. Por quê:
- Simples, leve, e excelente para “push do servidor → cliente”.
- Reconexão automática e menor complexidade que WebSocket para este caso.
Trade-offs:
- SSE é unidirecional (server → client). Para interação (Ack), usamos HTTP normal.
- Para casos avançados (bidirecional, multiplexação intensa), WebSocket pode ser melhor.
Decisão:
- Tracking: Redis é “nice-to-have” (cache). Sem Redis, continua registrando pings/alertas no Postgres.
- Notification: Redis é “safety-critical” para idempotência; sem Redis, preferimos falhar e usar DLQ (evita duplicar notificações).
Por quê:
- Notificações duplicadas podem causar ruído operacional (muito ruim).
- Melhor deixar a mensagem na DLQ e reprocessar quando o Redis voltar.
Decisão: métricas foram tratadas como requisito de arquitetura e não “extra”. Por quê:
- Em sistemas EDA, gargalos aparecem em filas, DLQ, outbox backlog e circuit breaker.
- Métricas permitem detectar degradação antes de virar incidente.
Trade-offs:
- Exige setup adicional (Prometheus/Grafana/exporters), mas melhora muito a qualidade do projeto.
cd infra
docker compose up -dcd apps/tracking-service
./mvnw spring-boot:run- Swagger:
http://localhost:8081/swagger-ui.html
cd apps/notification-service
npm i
npm run devcd apps/route-optimization-service
composer install
php artisan rabbit:consume-routecd apps/backoffice-dashboard
npm i
npm run dev- Dashboard:
http://localhost:3000 - DLQ monitor (ops):
http://localhost:3000/dlq
POST /deliveries— cria entrega (rota e destino)GET /deliveries— lista entregas com lastLocation e ETAPOST /tracking/pings— ingestão GPS e geração de alertasGET /alerts?onlyUnacked=true— lista alertas pendentesPOST /alerts/{id}/ack— trata alertaGET /stream/alerts— SSE de alertas em tempo realPOST /deliveries/{id}/route/recalculate— (opcional) recálculo manual via Outbox
O Backoffice possui uma página /dlq para:
- visualizar contagem de mensagens nas DLQs
- reprocessar mensagens (republicar no exchange) via RabbitMQ Management API
- Happy path (desvio → alerta → notifica → recalc → update)
- Duplicatas (idempotência)
- RabbitMQ fora (Outbox + Circuit Breaker)
- Consumers fora (fila acumula e drena depois)
- Mensagens inválidas (DLQ)
- Reprocessamento via Backoffice
