Motivação do estudo
O objetivo do NodeOrderSaga é entender, na prática, como orquestrar um fluxo de processamento assíncrono que pode falhar no meio do caminho — e o que fazer quando isso acontece. Processar pedidos envolve várias etapas (pagamento, estoque) que, numa arquitetura de microsserviços, não podem viver dentro de uma única transação de banco. A alternativa clássica é o Saga pattern: cada etapa é local e confirmada por conta própria, e se uma etapa mais adiante falhar, o orquestrador executa compensações — ações que desfazem o que já tinha sido feito — até o pedido chegar a um estado final consistente.
O estudo também é deliberadamente 100% Node.js de ponta a ponta: a API que recebe o pedido, o worker que consome a fila e processa a saga, e a ferramenta de build do frontend são todos Node. Isso isola a variável em estudo — orquestração de saga + fila + Kubernetes — sem misturar stacks diferentes, ao contrário de outros repositórios de referência deste autor que exploram .NET, Java ou C++ lado a lado.
O que foi feito
- API Express enxuta: três rotas de verdade —
POST /orderscria o pedido (grava no Postgres já comoPENDINGe enfileira o job) e devolve201imediatamente, sem esperar o processamento;GET /orders/:ideGET /orders/:id/historyconsultam o estado atual e a linha do tempo completa de transições. - Worker BullMQ como orquestrador da saga: um processo Node.js separado (própria imagem,
próprio Deployment, sem porta exposta) consome a fila
order-sagae conduz o pedido por uma máquina de estados sequencial —saga.jsé uma funçãoasyncsimples que aguarda cada etapa e chama a compensação em caso de falha, sem indireção de coreografia desnecessária para só dois participantes (pagamento e estoque). - Gatilho de falha determinístico: para poder demonstrar a compensação sob demanda (e não torcer
por aleatoriedade), um pedido falha a reserva de estoque quando
amount > 5000ousku = "OUT-OF-STOCK"— reproduzível por curl ou por um botão “preencher exemplo com falha” no frontend. - Status ao vivo via SSE: cada transição de estado que o worker grava no Postgres também é
publicada num canal Redis pub/sub (
order:<id>:status); a API expõe isso comoGET /orders/:id/events, um stream Server-Sent Events que o frontend consome para atualizar a linha do tempo em tempo real, sem polling. - Histórico auditável: toda transição de status grava uma linha em
order_events— o pedido em si guarda só o estado atual, mas a tabela de eventos preserva a sequência completa (inclusive as etapas de compensação) para inspeção posterior. - Deploy real em Kubernetes: manifests Kustomize (
k8s/base/, sem overlays) para os 5 componentes — Postgres, Redis, API, worker, frontend — testados de ponta a ponta contra um clusterkind/docker-desktoplocal, com as duas rotas da saga (sucesso e compensação) executadas viakubectl port-forwardcom os mesmos resultados do ambiente local em Docker Compose.
Como funciona
Visão geral do sistema
flowchart LR
subgraph Browser["Navegador"]
FE["Frontend React (Vite)"]
end
subgraph Cluster["API + Worker — Node.js"]
API["API :8080 — Express\nPOST /orders\nGET /orders/:id\nGET /orders/:id/events (SSE)"]
WORKER["Worker — BullMQ consumer\norquestra o Saga (saga.js)"]
end
PG[("PostgreSQL\norders / order_events")]
REDIS[("Redis\nfila order-saga (BullMQ)\n+ pub/sub de status")]
FE -- "POST /orders" --> API
FE -- "SSE GET /orders/:id/events" --> API
API -- "INSERT order (PENDING)" --> PG
API -- "enfileira job {orderId}" --> REDIS
WORKER -- "consome job" --> REDIS
WORKER -- "UPDATE status + INSERT event" --> PG
WORKER -- "publish order:id:status" --> REDIS
REDIS -- "pub/sub" --> API
Saga: caminho feliz vs. compensação
sequenceDiagram
participant FE as Frontend
participant API as API (Express)
participant Q as Redis (fila order-saga)
participant W as Worker (Saga)
participant DB as PostgreSQL
FE->>API: POST /orders {customerName, sku, quantity, amount}
API->>DB: INSERT order (status=PENDING)
API->>Q: enqueue job {orderId}
API-->>FE: 201 { id, status: PENDING }
FE->>API: GET /orders/:id/events (abre SSE)
Q->>W: job disponível
W->>DB: UPDATE status=PAYMENT_PROCESSING
W-->>API: publish (pub/sub)
API-->>FE: evento SSE: PAYMENT_PROCESSING
W->>DB: UPDATE status=PAYMENT_CONFIRMED
API-->>FE: evento SSE: PAYMENT_CONFIRMED
W->>DB: UPDATE status=STOCK_RESERVING
API-->>FE: evento SSE: STOCK_RESERVING
alt Caminho feliz — estoque disponível
W->>DB: UPDATE status=STOCK_CONFIRMED
API-->>FE: evento SSE: STOCK_CONFIRMED
W->>DB: UPDATE status=COMPLETED
API-->>FE: evento SSE: COMPLETED
else Compensação — amount > 5000 ou sku OUT-OF-STOCK
W->>DB: UPDATE status=STOCK_FAILED (failure_reason)
API-->>FE: evento SSE: STOCK_FAILED
Note over W: compensação: estornar o pagamento já confirmado
W->>DB: UPDATE status=COMPENSATING_PAYMENT
API-->>FE: evento SSE: COMPENSATING_PAYMENT
W->>DB: UPDATE status=CANCELLED
API-->>FE: evento SSE: CANCELLED
end
A saga é orquestrada, não coreografada: com apenas dois participantes (pagamento e estoque), uma
cadeia de serviços publicando e reagindo a eventos uns dos outros adicionaria indireção sem adicionar
segurança. O worker é o único coordenador — saga.js aguarda cada etapa em sequência e, ao detectar a
falha de estoque, chama explicitamente a compensação (COMPENSATING_PAYMENT) antes de fechar o pedido
como CANCELLED, preservando o motivo da falha em failure_reason.
Modelo de dados
erDiagram
ORDER ||--o{ ORDER_EVENT : "registra transições em"
ORDER {
uuid id PK
string customer_name
string sku
int quantity
numeric amount
string status "PENDING..CANCELLED/COMPLETED"
string failure_reason "preenchido só no caminho de compensação"
timestamptz created_at
timestamptz updated_at
}
ORDER_EVENT {
bigint id PK
uuid order_id FK
string status "snapshot do status nesse instante"
string note "ex.: Reservando estoque"
timestamptz created_at
}
A máquina de estados do pedido tem seis estados de caminho feliz e três de compensação:
PENDING → PAYMENT_PROCESSING → PAYMENT_CONFIRMED → STOCK_RESERVING → STOCK_CONFIRMED → COMPLETED
↓ (falha de estoque)
STOCK_FAILED → COMPENSATING_PAYMENT → CANCELLED
Stack
- ◆ BACKEND: Node.js + Express (API), Node.js + BullMQ (worker orquestrador da saga).
- ⇄ FILA / MENSAGERIA: Redis — BullMQ (Streams) para a fila
order-saga, pub/sub nativo para o fan-out de status ao vivo. - ▤ DADOS: PostgreSQL — tabelas
orders(estado atual) eorder_events(histórico completo). - ◈ FRONTEND: React + Vite, consumindo Server-Sent Events para a linha do tempo em tempo real.
- ⌂ INFRA: Docker Compose (local) e Kubernetes via Kustomize (
k8s/base/) — testado num clusterkind/docker-desktopreal.
Como rodar
# Docker Compose — sobe Postgres, Redis, API, worker e frontend
docker compose up -d --build
# Frontend → http://localhost:18083
# API → http://localhost:18080
# Caminho feliz
curl -X POST http://localhost:18080/orders \
-H "Content-Type: application/json" \
-d '{"customerName":"Alice","sku":"WIDGET-01","quantity":2,"amount":150}'
# Caminho de compensação (amount > 5000 ou sku OUT-OF-STOCK)
curl -X POST http://localhost:18080/orders \
-H "Content-Type: application/json" \
-d '{"customerName":"Bob","sku":"OUT-OF-STOCK","quantity":1,"amount":8000}'
# Acompanhar em tempo real
curl -N http://localhost:18080/orders/<id>/events
# Kubernetes — deploy num cluster kind/docker-desktop local (sem registry,
# imagens tagueadas :local + imagePullPolicy: IfNotPresent)
docker compose build
kubectl apply -k k8s/base
kubectl -n node-order-saga get pods
kubectl -n node-order-saga port-forward svc/api 30080:8080 # Código aberto
O NodeOrderSaga é open source, feito pra estudar e servir de referência de arquitetura.