A diferença entre código de demonstração e código de produção não está na lógica de negócio — está nas camadas que envolvem essa lógica. Um endpoint de API que busca um produto no banco de dados é trivial de escrever. O mesmo endpoint preparado para produção inclui validação de entrada, tratamento de erros estruturado, logging com contexto de rastreamento, métricas expostas para o Prometheus, circuit breaker para o banco de dados, graceful shutdown, healthchecks calibrados e testes que verificam não apenas o caminho feliz mas os cenários de falha.
Este artigo implementa os cinco microsserviços do capstone com todas essas camadas integradas. Cada serviço é apresentado com seu núcleo funcional e as práticas operacionais que o tornam adequado para produção — não como adições posteriores, mas como parte intrínseca do design.
Estrutura Compartilhada: O Pacote Core
Código repetido entre serviços é um passivo. O pacote @loja/core contém as abstrações compartilhadas — logging, métricas, tracing, resiliência e configuração — que todos os serviços consomem:
// packages/core/src/index.js
module.exports = {
...require('./logger'),
...require('./metrics'),
...require('./tracing'),
...require('./resilience'),
...require('./config'),
...require('./health'),
};
// packages/core/src/logger.js
const pino = require('pino');
function criarLogger(contexto = {}) {
return pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
base: {
servico: process.env.SERVICE_NAME,
versao: process.env.APP_VERSION || 'desconhecida',
ambiente: process.env.NODE_ENV,
...contexto,
},
timestamp: pino.stdTimeFunctions.isoTime,
redact: {
paths: [
'password', 'senha', 'token', 'secret',
'authorization', 'cookie', 'cartao',
'*.password', '*.senha', '*.token',
],
censor: '[REDACTED]',
},
});
}
module.exports = { criarLogger };
// packages/core/src/metrics.js
const client = require('prom-client');
// Registra métricas padrão do Node.js (CPU, memória, event loop)
client.collectDefaultMetrics({
prefix: `${process.env.SERVICE_NAME?.replace(/-/g, '_')}_`,
});
// Métricas HTTP padrão para todos os serviços
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duração das requisições HTTP em segundos',
labelNames: ['method', 'route', 'status_code', 'service'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total de requisições HTTP',
labelNames: ['method', 'route', 'status_code', 'service'],
});
function middlewareMetricas(req, res, next) {
const inicio = Date.now();
res.on('finish', () => {
const duracao = (Date.now() - inicio) / 1000;
const labels = {
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode,
service: process.env.SERVICE_NAME,
};
httpRequestDuration.observe(labels, duracao);
httpRequestsTotal.inc(labels);
});
next();
}
async function handleMetricas(req, res) {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
}
module.exports = { middlewareMetricas, handleMetricas, client };
Serviço de Catálogo
O serviço de catálogo tem as maiores demandas de leitura do sistema. O design prioriza caching agressivo com Redis e uma Read Replica do PostgreSQL para escalar leituras horizontalmente:
// services/catalog-service/src/server.js
const express = require('express');
const { criarLogger, middlewareMetricas, handleMetricas } = require('@loja/core');
const { configurarTracing } = require('./tracing');
const produtosRouter = require('./routes/produtos.routes');
const healthRouter = require('./routes/health.routes');
const { inicializarConexoes, encerrarConexoes } = require('./db/conexoes');
configurarTracing();
const app = express();
const logger = criarLogger({ servico: 'catalog-service' });
app.use(express.json({ limit: '1mb' }));
app.use(middlewareMetricas);
// Middleware de contexto de rastreamento
app.use((req, res, next) => {
req.logger = logger.child({
traceId: req.headers['x-trace-id'] || crypto.randomUUID(),
method: req.method,
path: req.path,
});
next();
});
app.use('/health', healthRouter);
app.use('/metrics', handleMetricas);
app.use('/produtos', produtosRouter);
// Handler de erros global
app.use((err, req, res, next) => {
const status = err.status || 500;
req.logger.error({
err: {
message: err.message,
stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined,
},
status,
}, 'Erro não tratado');
res.status(status).json({
erro: status < 500 ? err.message : 'Erro interno do servidor',
codigo: err.codigo || 'ERRO_INTERNO',
traceId: req.logger.bindings().traceId,
});
});
let servidor;
async function iniciar() {
await inicializarConexoes();
servidor = app.listen(process.env.PORT || 3001, () => {
logger.info({ porta: process.env.PORT || 3001 }, 'Catalog service iniciado');
});
}
async function encerrar(sinal) {
logger.info({ sinal }, 'Encerrando catalog service...');
servidor.close(async () => {
await encerrarConexoes();
logger.info('Encerramento concluído');
process.exit(0);
});
setTimeout(() => {
logger.error('Timeout de encerramento — forçando saída');
process.exit(1);
}, 30000);
}
process.on('SIGTERM', () => encerrar('SIGTERM'));
process.on('SIGINT', () => encerrar('SIGINT'));
iniciar().catch(err => {
logger.fatal({ err }, 'Falha na inicialização');
process.exit(1);
});
// services/catalog-service/src/routes/produtos.routes.js
const router = require('express').Router();
const { param, query, validationResult } = require('express-validator');
const ProdutoService = require('../services/produto.service');
const validarUUID = (campo) =>
param(campo).isUUID(4).withMessage(`${campo} deve ser um UUID válido`);
function checarValidacao(req, res, next) {
const erros = validationResult(req);
if (!erros.isEmpty()) {
return res.status(400).json({
erro: 'Dados inválidos',
detalhes: erros.array(),
});
}
next();
}
// GET /produtos — lista com paginação e filtros
router.get('/',
query('pagina').optional().isInt({ min: 1 }).toInt(),
query('limite').optional().isInt({ min: 1, max: 100 }).toInt(),
query('categoria_id').optional().isUUID(4),
query('busca').optional().isString().trim().isLength({ max: 100 }),
checarValidacao,
async (req, res, next) => {
try {
const resultado = await ProdutoService.listar({
pagina: req.query.pagina || 1,
limite: req.query.limite || 20,
categoriaId: req.query.categoria_id,
busca: req.query.busca,
}, req.logger);
res.json(resultado);
} catch (err) {
next(err);
}
}
);
// GET /produtos/:id
router.get('/:id',
validarUUID('id'),
checarValidacao,
async (req, res, next) => {
try {
const produto = await ProdutoService.buscarPorId(
req.params.id,
req.logger
);
if (!produto) {
return res.status(404).json({
erro: 'Produto não encontrado',
codigo: 'PRODUTO_NAO_ENCONTRADO',
});
}
res.json(produto);
} catch (err) {
next(err);
}
}
);
// GET /produtos/:id/estoque — endpoint crítico para o order-service
router.get('/:id/estoque',
validarUUID('id'),
query('quantidade').isInt({ min: 1 }).toInt(),
checarValidacao,
async (req, res, next) => {
try {
const estoque = await ProdutoService.verificarEstoque(
req.params.id,
req.query.quantidade,
req.logger
);
res.json(estoque);
} catch (err) {
next(err);
}
}
);
module.exports = router;
// services/catalog-service/src/services/produto.service.js
const { CircuitBreaker } = require('@loja/core');
const cache = require('../cache/cache.service');
const ProdutoRepository = require('../repositories/produto.repository');
const { Histogram } = require('prom-client');
const duracaoCacheOp = new Histogram({
name: 'catalog_cache_operation_seconds',
help: 'Duração das operações de cache no catalog-service',
labelNames: ['operacao', 'hit'],
buckets: [0.001, 0.005, 0.01, 0.025, 0.05],
});
const dbCircuitBreaker = new CircuitBreaker({
nome: 'catalog-postgres',
limiarFalhas: 5,
timeoutAbertura: 30000,
timeoutRequisicao: 3000,
});
class ProdutoService {
async buscarPorId(id, logger) {
const chaveCache = `produto:${id}`;
const timer = duracaoCacheOp.startTimer({ operacao: 'get' });
// Tenta o cache primeiro
const cached = await cache.get(chaveCache);
if (cached) {
timer({ hit: 'true' });
logger.debug({ id, fonte: 'cache' }, 'Produto encontrado no cache');
return cached;
}
timer({ hit: 'false' });
// Cache miss — busca no banco com circuit breaker
const produto = await dbCircuitBreaker.executar(
() => ProdutoRepository.buscarPorId(id),
// Fallback: retorna null em vez de propagar o erro
() => null
);
if (produto) {
// Armazena no cache com TTL de 10 minutos
await cache.set(chaveCache, produto, 600);
logger.debug({ id, fonte: 'banco' }, 'Produto buscado do banco');
}
return produto;
}
async listar(filtros, logger) {
const chaveCache = `produtos:lista:${JSON.stringify(filtros)}`;
return cache.getOuBuscar(
chaveCache,
() => dbCircuitBreaker.executar(
() => ProdutoRepository.listar(filtros)
),
120 // TTL de 2 minutos para listas
);
}
async verificarEstoque(produtoId, quantidade, logger) {
// Estoque não é cacheado — precisa ser sempre atual
const estoque = await dbCircuitBreaker.executar(
() => ProdutoRepository.buscarEstoque(produtoId)
);
if (!estoque) {
return { disponivel: false, quantidade_disponivel: 0 };
}
return {
disponivel: estoque.quantidade >= quantidade,
quantidade_disponivel: estoque.quantidade,
};
}
}
module.exports = new ProdutoService();
Serviço de Pedidos
O serviço de pedidos é o mais crítico do sistema — orquestra o checkout coordenando chamadas ao serviço de catálogo, ao gateway de pagamento e às filas SQS:
// services/order-service/src/services/checkout.service.js
const { ClienteHTTPResilient, criarLogger } = require('@loja/core');
const PedidoRepository = require('../repositories/pedido.repository');
const AuditoriaService = require('../auditoria/auditoria.service');
const SQSPublisher = require('../queue/sqs.publisher');
const logger = criarLogger({ servico: 'order-service' });
const clienteCatalogo = new ClienteHTTPResilient(
process.env.CATALOG_SERVICE_URL,
{ nome: 'catalog-service', limiarFalhas: 5 }
);
const clientePagamento = new ClienteHTTPResilient(
process.env.PAYMENT_GATEWAY_URL,
{
nome: 'payment-gateway',
limiarFalhas: 3,
timeoutRequisicao: 10000, // Pagamento pode ser lento
}
);
class CheckoutService {
async iniciarCheckout(usuarioId, itens, enderecoEntrega, traceId) {
const logCtx = logger.child({ traceId, usuarioId });
logCtx.info({ qtdItens: itens.length }, 'Iniciando checkout');
// 1. Valida disponibilidade de todos os itens em paralelo
const verificacoes = await Promise.allSettled(
itens.map(item =>
clienteCatalogo.get(
`/produtos/${item.produto_id}/estoque?quantidade=${item.quantidade}`
)
)
);
const itensSemEstoque = [];
for (let i = 0; i < verificacoes.length; i++) {
const { status, value, reason } = verificacoes[i];
if (status === 'rejected') {
logCtx.warn(
{ produtoId: itens[i].produto_id, erro: reason?.message },
'Falha ao verificar estoque — assumindo disponível'
);
// Falha na verificação não bloqueia o checkout
// O sistema de estoque tem sua própria consistência
continue;
}
if (!value.disponivel) {
itensSemEstoque.push({
produto_id: itens[i].produto_id,
solicitado: itens[i].quantidade,
disponivel: value.quantidade_disponivel,
});
}
}
if (itensSemEstoque.length > 0) {
logCtx.info({ itensSemEstoque }, 'Itens sem estoque suficiente');
return {
sucesso: false,
erro: 'ESTOQUE_INSUFICIENTE',
itens_sem_estoque: itensSemEstoque,
};
}
// 2. Busca os dados dos produtos para o snapshot do pedido
const dadosProdutos = await Promise.all(
itens.map(item =>
clienteCatalogo.get(`/produtos/${item.produto_id}`)
)
);
// 3. Calcula o total do pedido
const itensProcessados = itens.map((item, i) => ({
produto_id: item.produto_id,
nome_produto: dadosProdutos[i].nome,
preco_unitario: dadosProdutos[i].preco_promocional || dadosProdutos[i].preco,
quantidade: item.quantidade,
subtotal: (dadosProdutos[i].preco_promocional || dadosProdutos[i].preco)
* item.quantidade,
}));
const subtotal = itensProcessados.reduce((acc, item) => acc + item.subtotal, 0);
const frete = this._calcularFrete(enderecoEntrega, subtotal);
const total = subtotal + frete;
// 4. Cria o pedido no banco com status "aguardando_pagamento"
const pedido = await PedidoRepository.criar({
usuario_id: usuarioId,
status: 'aguardando_pagamento',
itens: itensProcessados,
subtotal,
frete,
total,
endereco_entrega: enderecoEntrega,
metadata: { traceId },
});
logCtx.info({ pedidoId: pedido.id, total }, 'Pedido criado');
await AuditoriaService.registrar({
tipo: 'pedido.criado',
resultado: 'sucesso',
ator: { id: usuarioId, tipo: 'usuario' },
recurso: { tipo: 'pedido', id: pedido.id },
detalhes: { total, qtdItens: itens.length },
traceId,
});
return { sucesso: true, pedido };
}
async processarPagamento(pedidoId, dadosPagamento, traceId) {
const logCtx = logger.child({ traceId, pedidoId });
const pedido = await PedidoRepository.buscarPorId(pedidoId);
if (!pedido || pedido.status !== 'aguardando_pagamento') {
return { sucesso: false, erro: 'PEDIDO_NAO_ENCONTRADO_OU_STATUS_INVALIDO' };
}
logCtx.info({ valor: pedido.total }, 'Processando pagamento');
let resultadoPagamento;
try {
resultadoPagamento = await clientePagamento.post('/cobrar', {
valor: pedido.total,
moeda: 'BRL',
descricao: `Pedido #${pedidoId}`,
referencia: pedidoId,
pagamento: dadosPagamento,
});
} catch (erro) {
logCtx.error({ erro: erro.message }, 'Falha ao processar pagamento');
await PedidoRepository.atualizarStatus(pedidoId, 'cancelado', {
motivo_cancelamento: 'falha_pagamento',
erro_pagamento: erro.message,
});
return { sucesso: false, erro: 'FALHA_PAGAMENTO' };
}
if (!resultadoPagamento.aprovado) {
await PedidoRepository.atualizarStatus(pedidoId, 'cancelado', {
motivo_cancelamento: 'pagamento_recusado',
codigo_recusa: resultadoPagamento.codigo_recusa,
});
return {
sucesso: false,
erro: 'PAGAMENTO_RECUSADO',
codigo_recusa: resultadoPagamento.codigo_recusa,
};
}
// 5. Atualiza o pedido para "pago" e publica evento
await PedidoRepository.atualizarStatus(pedidoId, 'pago', {
id_transacao: resultadoPagamento.id_transacao,
confirmado_em: new Date(),
});
// Publica evento de pedido confirmado — consome assincronamente
// pelos serviços de notificação e catálogo (atualização de estoque)
await SQSPublisher.publicar('pedido-confirmado', {
pedidoId,
usuarioId: pedido.usuario_id,
total: pedido.total,
itens: pedido.itens,
confirmedAt: new Date().toISOString(),
traceId,
});
logCtx.info({ transacaoId: resultadoPagamento.id_transacao }, 'Pagamento aprovado');
return {
sucesso: true,
pedido: await PedidoRepository.buscarPorId(pedidoId),
};
}
_calcularFrete(endereco, subtotal) {
// Frete grátis acima de R$ 200
if (subtotal >= 200) return 0;
// Simplificado — em produção integraria com serviço de frete
return 15.90;
}
}
module.exports = new CheckoutService();
Serviço de Notificações
O serviço de notificações é um consumidor SQS puro — sem endpoints HTTP de negócio, apenas processamento de mensagens de fila:
// services/notification-service/src/consumer/sqs.consumer.js
const { SQSClient, ReceiveMessageCommand,
DeleteMessageCommand, ChangeMessageVisibilityCommand } = require('@aws-sdk/client-sqs');
const { criarLogger } = require('@loja/core');
const EmailService = require('../services/email.service');
const { Counter, Histogram } = require('prom-client');
const logger = criarLogger({ servico: 'notification-service' });
const sqs = new SQSClient({ region: process.env.AWS_REGION });
const mensagensProcessadas = new Counter({
name: 'notifications_messages_processed_total',
help: 'Total de mensagens SQS processadas',
labelNames: ['fila', 'resultado'],
});
const duracaoProcessamento = new Histogram({
name: 'notifications_processing_duration_seconds',
help: 'Duração do processamento de mensagens',
labelNames: ['fila', 'tipo'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
const FILAS = {
'pedido-confirmado': {
url: process.env.SQS_URL_PEDIDO_CONFIRMADO,
handler: processarPedidoConfirmado,
},
'pedido-cancelado': {
url: process.env.SQS_URL_PEDIDO_CANCELADO,
handler: processarPedidoCancelado,
},
};
async function processarPedidoConfirmado(corpo, msgLogger) {
const { pedidoId, usuarioId, total, traceId } = corpo;
msgLogger.info({ pedidoId, total }, 'Enviando confirmação de pedido');
// Busca email do usuário via user-service
const usuario = await fetch(
`${process.env.USER_SERVICE_URL}/usuarios/${usuarioId}`,
{ headers: { 'x-trace-id': traceId } }
).then(r => r.json());
await EmailService.enviar({
para: usuario.email,
assunto: `Pedido #${pedidoId} confirmado!`,
template: 'pedido-confirmado',
dados: {
nome: usuario.nome,
pedidoId,
total: total.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
linkAcompanhamento: `${process.env.APP_URL}/pedidos/${pedidoId}`,
},
});
msgLogger.info({ pedidoId, email: usuario.email }, 'Email de confirmação enviado');
}
async function processarPedidoCancelado(corpo, msgLogger) {
const { pedidoId, usuarioId, motivoCancelamento } = corpo;
const usuario = await fetch(
`${process.env.USER_SERVICE_URL}/usuarios/${usuarioId}`
).then(r => r.json());
await EmailService.enviar({
para: usuario.email,
assunto: `Pedido #${pedidoId} cancelado`,
template: 'pedido-cancelado',
dados: {
nome: usuario.nome,
pedidoId,
motivo: motivoCancelamento,
},
});
}
// Loop de polling da fila
async function iniciarConsumidor(nomeFila, config) {
logger.info({ fila: nomeFila }, 'Iniciando consumidor SQS');
while (true) {
try {
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
QueueUrl: config.url,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20, // Long polling — reduz custos e latência
MessageAttributeNames: ['All'],
}));
await Promise.allSettled(
Messages.map(msg => processarMensagem(msg, nomeFila, config))
);
} catch (erro) {
logger.error({ erro: erro.message, fila: nomeFila }, 'Erro no loop de polling');
// Backoff em caso de erro para não sobrecarregar a fila
await new Promise(r => setTimeout(r, 5000));
}
}
}
async function processarMensagem(msg, nomeFila, config) {
const timer = duracaoProcessamento.startTimer({ fila: nomeFila });
const corpo = JSON.parse(msg.Body);
const msgLogger = logger.child({
messageId: msg.MessageId,
fila: nomeFila,
traceId: corpo.traceId,
});
try {
await config.handler(corpo, msgLogger);
// Remove a mensagem da fila após processamento bem-sucedido
await sqs.send(new DeleteMessageCommand({
QueueUrl: config.url,
ReceiptHandle: msg.ReceiptHandle,
}));
mensagensProcessadas.inc({ fila: nomeFila, resultado: 'sucesso' });
timer({ tipo: corpo.tipo || 'desconhecido' });
} catch (erro) {
msgLogger.error({ erro: erro.message }, 'Falha ao processar mensagem');
mensagensProcessadas.inc({ fila: nomeFila, resultado: 'falha' });
// Não deleta a mensagem — voltará para a fila após o timeout de visibilidade
// Após maxReceiveCount tentativas, vai para a DLQ automaticamente
}
}
// Inicia todos os consumidores em paralelo
async function iniciar() {
await Promise.all(
Object.entries(FILAS).map(([nome, config]) =>
iniciarConsumidor(nome, config)
)
);
}
module.exports = { iniciar };
Manifestos Kubernetes: Deployment Completo
# infrastructure/kubernetes/services/order-service/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: producao
annotations:
kubernetes.io/change-cause: "Deploy inicial do order-service"
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: order-service
versao: "1.0.0"
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3003"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: order-service
terminationGracePeriodSeconds: 60
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: order-service
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: order-service
image: ghcr.io/empresa/order-service:1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3003
name: http
env:
- name: NODE_ENV
value: production
- name: PORT
value: "3003"
- name: SERVICE_NAME
value: order-service
- name: CATALOG_SERVICE_URL
value: http://catalog-service.producao.svc.cluster.local
- name: USER_SERVICE_URL
value: http://user-service.producao.svc.cluster.local
# Secrets injetados via External Secrets Operator
envFrom:
- secretRef:
name: order-service-secrets
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 3003
initialDelaySeconds: 30
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3003
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
imagePullSecrets:
- name: ghcr-credentials
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: producao
spec:
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 3003
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service
namespace: producao
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: order-service-secrets
namespace: producao
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: order-service-secrets
creationPolicy: Owner
dataFrom:
- extract:
key: loja/producao/order
Ambiente de Desenvolvimento Local
# docker-compose.yml — desenvolvimento local completo
version: '3.8'
services:
postgres-catalog:
image: postgres:16-alpine
environment:
POSTGRES_DB: catalog_db
POSTGRES_USER: catalog_admin
POSTGRES_PASSWORD: dev_password
ports:
- "5432:5432"
volumes:
- postgres_catalog:/var/lib/postgresql/data
- ./services/catalog-service/migrations:/docker-entrypoint-initdb.d
postgres-order:
image: postgres:16-alpine
environment:
POSTGRES_DB: order_db
POSTGRES_USER: order_admin
POSTGRES_PASSWORD: dev_password
ports:
- "5433:5432"
volumes:
- postgres_order:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
SERVICES: sqs,secretsmanager
DEFAULT_REGION: us-east-1
volumes:
- ./scripts/local-dev/localstack-init.sh:/etc/localstack/init/ready.d/init.sh
catalog-service:
build:
context: .
dockerfile: services/catalog-service/Dockerfile
target: development
ports:
- "3001:3001"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://catalog_admin:dev_password@postgres-catalog:5432/catalog_db
REDIS_URL: redis://redis:6379
volumes:
- ./services/catalog-service/src:/app/src
depends_on:
- postgres-catalog
- redis
order-service:
build:
context: .
dockerfile: services/order-service/Dockerfile
target: development
ports:
- "3003:3003"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://order_admin:dev_password@postgres-order:5432/order_db
CATALOG_SERVICE_URL: http://catalog-service:3001
USER_SERVICE_URL: http://user-service:3002
SQS_URL_PEDIDO_CONFIRMADO: http://localstack:4566/000000000000/pedido-confirmado
AWS_ENDPOINT_URL: http://localstack:4566
volumes:
- ./services/order-service/src:/app/src
depends_on:
- postgres-order
- catalog-service
- localstack
notification-service:
build:
context: .
dockerfile: services/notification-service/Dockerfile
target: development
environment:
NODE_ENV: development
SQS_URL_PEDIDO_CONFIRMADO: http://localstack:4566/000000000000/pedido-confirmado
USER_SERVICE_URL: http://user-service:3002
AWS_ENDPOINT_URL: http://localstack:4566
volumes:
- ./services/notification-service/src:/app/src
depends_on:
- localstack
volumes:
postgres_catalog:
postgres_order:
O Que Vem a Seguir
Com a infraestrutura e os serviços implementados, o próximo artigo constrói o pipeline completo de CI/CD que conecta um commit no repositório a um deploy em produção — com testes, scanning de segurança, build de imagem, deploy via ArgoCD e verificação pós-deploy automatizada.
Referências para Aprofundamento
Node.js em produção - Node.js Best Practices — github.com/goldbergyoni — Repositório com mais de 80 boas práticas para Node.js em produção, organizadas por categoria com exemplos e justificativas detalhadas. - Pino Logger Documentation — getpino.io — Documentação do Pino com guia de configuração para produção, serializers customizados e integração com sistemas de logging centralizados.
Padrões de microsserviços - Saga Pattern — microservices.io — Descrição completa do padrão Saga para transações distribuídas em microsserviços, com exemplos de implementação coreografada e orquestrada. - AWS SQS Best Practices — docs.aws.amazon.com — Guia oficial de boas práticas para SQS, incluindo configuração de dead-letter queues, idempotência de consumidores e otimização de custos.