Projeto Capstone: Arquitetura do Sistema Completo

[316] Projeto Capstone: Arquitetura do Sistema Completo

A arquitetura do projeto que integra toda a série em um sistema de produção: uma plataforma de e-commerce com cinco microsserviços no EKS, os Architecture Decision Records que registram cada trade-off, a comunicação híbrida entre síncrono e filas, o banco por serviço, os contratos OpenAPI e os SLOs.
DevOps

23 min de leitura

Todos os artigos anteriores (Resiliência e Chaos Engineering, Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento, Cultura DevOps: Maturidade, Postmortems e Melhoria Contínua) cobriram, em progressão cuidadosa, cada camada da engenharia de software moderna: o terminal Linux, o controle de versão com Git, containers com Docker, pipelines de CI/CD, infraestrutura como código com Terraform, monitoramento com Prometheus e Grafana, AWS em profundidade, Kubernetes, segurança com DevSecOps, compliance, FinOps, resiliência e cultura organizacional. Cada artigo tratou de sua camada com profundidade — ferramentas, padrões, código funcional, decisões de arquitetura.

O que falta é a integração. Sistemas reais não são coleções de tecnologias operando em silos — são organismos onde cada componente interage com os demais de maneiras que criam propriedades emergentes, tanto positivas quanto negativas. Um pipeline de CI/CD excelente tem valor limitado se a infraestrutura que ele provisiona não está monitorada. Observabilidade sofisticada não ajuda se o processo de deploy não tem estratégia de rollback. Segurança no código não basta se a infraestrutura está mal configurada.

O projeto capstone desta série integra todos esses componentes em um sistema completo de produção: uma plataforma de e-commerce com microsserviços, deployed no EKS, com pipeline completo de CI/CD, observabilidade full-stack, segurança em camadas e práticas de FinOps. Não é um sistema simplificado para fins didáticos — é a arquitetura que seria usada em produção real, com as decisões de trade-off que sistemas reais exigem.

Este artigo define a arquitetura. Os artigos 49 e 50 implementam a infraestrutura e os serviços. Os artigos 51 e 52 implementam o pipeline completo e as operações em produção.

O Sistema: Plataforma de E-Commerce

A plataforma escolhida para o capstone é um sistema de e-commerce — domínio suficientemente rico para exigir todas as práticas abordadas no curso, mas familiar o bastante para que o foco permaneça nas práticas de engenharia e não na lógica de negócio.

O sistema é composto por cinco microsserviços, cada um com responsabilidade bem definida:

Serviço de Catálogo — gerencia produtos, categorias, preços e estoque. Alta taxa de leitura, baixa taxa de escrita. Candidato ideal para caching agressivo.

Serviço de Usuários — gerencia contas, autenticação e perfis. Dados sensíveis sob LGPD. Requer auditoria de todos os acessos.

Serviço de Pedidos — orquestra o fluxo de compra: criação de carrinho, checkout, pagamento e fulfillment. Core do negócio — requer alta disponibilidade e consistência eventual entre serviços.

Serviço de Notificações — envia emails, SMS e notificações push. Assíncrono por natureza — opera sobre filas SQS. Tolerante a latência, mas não a perda de mensagens.

API Gateway — ponto único de entrada para clientes externos. Roteamento, autenticação, rate limiting e agregação de respostas.

Diagrama de Arquitetura

┌─────────────────────────────────────────────────────────────────────┐
│                           INTERNET                                   │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                ┌──────────▼──────────┐
                │    CloudFront CDN    │
                │  WAF + Shield Std   │
                └──────────┬──────────┘
                           │
              ┌────────────▼────────────┐
              │  Application LB (ALB)   │
              │  Route53 → api.loja.com │
              └────────────┬────────────┘
                           │
    ┌──────────────────────▼──────────────────────────┐
    │                  EKS Cluster                     │
    │  ┌────────────────────────────────────────────┐ │
    │  │           Namespace: producao               │ │
    │  │                                            │ │
    │  │  ┌────────────┐    ┌──────────────────┐   │ │
    │  │  │ API Gateway│    │ Serviço Catálogo  │   │ │
    │  │  │  (3 pods)  │───▶│    (5 pods)      │   │ │
    │  │  └─────┬──────┘    └────────┬─────────┘   │ │
    │  │        │                    │              │ │
    │  │  ┌─────▼──────┐    ┌────────▼─────────┐   │ │
    │  │  │  Serviço   │    │    Serviço       │   │ │
    │  │  │  Usuários  │    │    Pedidos       │   │ │
    │  │  │  (3 pods)  │    │    (5 pods)      │   │ │
    │  │  └────────────┘    └────────┬─────────┘   │ │
    │  │                             │              │ │
    │  │                    ┌────────▼─────────┐   │ │
    │  │                    │    Serviço       │   │ │
    │  │                    │  Notificações    │   │ │
    │  │                    │    (2 pods)      │   │ │
    │  │                    └──────────────────┘   │ │
    │  └────────────────────────────────────────────┘ │
    │                                                  │
    │  ┌────────────────────────────────────────────┐ │
    │  │          Namespace: monitoring              │ │
    │  │  Prometheus · Grafana · Loki · Tempo        │ │
    │  └────────────────────────────────────────────┘ │
    └──────────────────────────────────────────────────┘
                           │
    ┌──────────────────────▼──────────────────────────┐
    │                 AWS Managed Services             │
    │                                                  │
    │  ┌─────────────┐  ┌──────────────┐              │
    │  │ RDS Postgres │  │ ElastiCache  │              │
    │  │  Multi-AZ   │  │    Redis     │              │
    │  │             │  │  (cluster)   │              │
    │  └─────────────┘  └──────────────┘              │
    │                                                  │
    │  ┌─────────────┐  ┌──────────────┐              │
    │  │  SQS Queues │  │      S3      │              │
    │  │ notificações│  │  assets +    │              │
    │  │   + DLQ     │  │   backups    │              │
    │  └─────────────┘  └──────────────┘              │
    │                                                  │
    │  ┌─────────────┐  ┌──────────────┐              │
    │  │ Secrets Mgr │  │     KMS      │              │
    │  │ credenciais │  │ criptografia │              │
    │  └─────────────┘  └──────────────┘              │
    └──────────────────────────────────────────────────┘

Decisões de Arquitetura e Trade-offs

Toda arquitetura é o resultado de decisões que priorizam algumas propriedades em detrimento de outras. Documentar essas decisões — com a alternativa considerada e o motivo da escolha — é uma prática essencial que o capstone demonstra através de Architecture Decision Records:

ADR-001: Microsserviços vs Monolito Modular

# ADR-001: Adoção de Microsserviços

**Status:** Aceito
**Data:** 2025-01-15
**Decisores:** Time de Arquitetura

## Contexto

O sistema precisa suportar times independentes trabalhando em
diferentes domínios de negócio com ciclos de deploy independentes.
O catálogo tem requisitos de leitura muito diferentes do serviço
de pedidos, que tem requisitos de consistência que o serviço de
notificações não compartilha.

## Decisão

Adotar arquitetura de microsserviços com cinco serviços principais,
cada um com seu próprio banco de dados (database-per-service pattern).

## Alternativa Considerada

Monolito modular — código organizado em módulos com fronteiras claras
mas deployado como uma única unidade. Menor complexidade operacional,
deploy atômico, sem latência de rede entre módulos.

## Motivo da Escolha

A necessidade de deploy independente entre catálogo (deploy a cada
mudança de produto, várias vezes ao dia) e pedidos (deploy mais
controlado, requer testes extensivos) foi o fator determinante.
Times independentes precisam de deployabilidade independente.

## Consequências

Positivas: Deploy independente por serviço, scaling independente,
isolamento de falhas, autonomia de times.

Negativas: Complexidade operacional elevada (requer Kubernetes,
service discovery, tracing distribuído), latência de rede entre
serviços, consistência eventual entre bancos de dados.

## Critério de Revisão

Se o número de microsserviços superar 15 sem crescimento
proporcional do time, reavaliar consolidação de serviços menores.

ADR-002: Comunicação entre Serviços

# ADR-002: Padrão de Comunicação entre Serviços

**Status:** Aceito
**Data:** 2025-01-15

## Contexto

Serviços precisam se comunicar. As opções principais são:
comunicação síncrona via HTTP/gRPC, ou comunicação assíncrona
via mensageria (SQS, Kafka, EventBridge).

## Decisão

Comunicação híbrida:
- **Síncrona (HTTP/REST)** para consultas que precisam de
  resposta imediata: API Gateway → serviços, Pedidos → Catálogo
  (verificar estoque), Pedidos → Usuários (autenticação)
- **Assíncrona (SQS)** para eventos que não requerem resposta
  imediata: Pedidos → Notificações (confirmar pedido),
  Pedidos → Catálogo (atualizar estoque após confirmação)

## Padrão para Comunicação Síncrona

Todos os clientes HTTP inter-serviço usam o ClienteHTTPResilient
com circuit breaker e retry — implementado no artigo Performance e FinOps: Otimizando Custo e Velocidade na Cloud.

## Padrão para Comunicação Assíncrona

Produtor publica evento no SQS. Consumidor processa com
visibilidade de 30s e até 3 tentativas antes de mover para DLQ.
Eventos têm schema versionado para compatibilidade.

## Consequências

A combinação evita o acoplamento temporal completo (tudo síncrono)
e a complexidade de tornar tudo assíncrono onde a simplicidade
do request-response é adequada.

ADR-003: Estratégia de Banco de Dados

# ADR-003: Banco de Dados por Serviço

**Status:** Aceito
**Data:** 2025-01-15

## Decisão

Cada microsserviço tem seu próprio banco de dados ou schema isolado.
Nenhum serviço acessa diretamente o banco de outro serviço.

Distribuição dos bancos:
- **Catálogo:** PostgreSQL próprio (leituras intensas, writes moderados)
- **Usuários:** PostgreSQL próprio (dados sensíveis LGPD, acesso auditado)
- **Pedidos:** PostgreSQL próprio (consistência transacional crítica)
- **Notificações:** Sem banco próprio — usa SQS como fonte de verdade

Redis compartilhado para cache — não contém dados de negócio,
apenas cache de leitura com TTL. A perda do Redis é tolerável
(degrada performance, não corrompe dados).

## Alternativa Considerada

Schema por serviço no mesmo banco de dados PostgreSQL — menor
custo de operação, transações entre serviços possíveis.

## Motivo da Rejeição

Schema compartilhado cria acoplamento de infraestrutura: uma
migration mal feita em um serviço pode bloquear os demais.
O isolamento completo tem custo financeiro maior mas garante
autonomia operacional total.

Estrutura do Repositório

O projeto capstone usa um monorepo com fronteiras claras entre serviços:

loja-plataforma/
│
├── services/
│   ├── api-gateway/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── catalog-info.yaml
│   │
│   ├── catalog-service/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── catalog-info.yaml
│   │
│   ├── user-service/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── catalog-info.yaml
│   │
│   ├── order-service/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   └── catalog-info.yaml
│   │
│   └── notification-service/
│       ├── src/
│       ├── Dockerfile
│       ├── package.json
│       └── catalog-info.yaml
│
├── infrastructure/
│   ├── terraform/
│   │   ├── environments/
│   │   │   ├── staging/
│   │   │   └── production/
│   │   └── modules/
│   │       ├── eks/
│   │       ├── rds/
│   │       ├── elasticache/
│   │       ├── networking/
│   │       └── observability/
│   └── kubernetes/
│       ├── base/
│       │   ├── namespaces.yaml
│       │   ├── network-policies.yaml
│       │   └── resource-quotas.yaml
│       ├── services/
│       │   ├── api-gateway/
│       │   ├── catalog-service/
│       │   ├── user-service/
│       │   ├── order-service/
│       │   └── notification-service/
│       └── platform/
│           ├── argocd/
│           ├── karpenter/
│           ├── prometheus/
│           └── external-secrets/
│
├── .github/
│   └── workflows/
│       ├── ci-services.yml
│       ├── deploy-infrastructure.yml
│       ├── deploy-services.yml
│       └── security-scan.yml
│
├── docs/
│   ├── adr/               # Architecture Decision Records
│   ├── runbooks/          # Runbooks operacionais
│   └── mkdocs.yml
│
└── scripts/
    ├── local-dev/
    ├── chaos/
    └── finops/

Contratos de API entre Serviços

Os contratos entre serviços são documentados como especificações OpenAPI e mantidos no repositório. Mudanças incompatíveis requerem versionamento da API:

# services/catalog-service/openapi.yaml
openapi: "3.1.0"
info:
  title: Catalog Service API
  version: "2.0.0"
  description: |
    API interna do serviço de catálogo.
    Versão 2.0 — não é compatível com v1 (removido campo `preco_antigo`).

servers:
  - url: http://catalog-service.producao.svc.cluster.local
    description: Cluster interno (DNS Kubernetes)

paths:
  /produtos/{id}:
    get:
      operationId: buscarProduto
      summary: Busca produto por ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Produto encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Produto'
        "404":
          $ref: '#/components/responses/NaoEncontrado'
        "503":
          $ref: '#/components/responses/ServicoIndisponivel'

  /produtos/{id}/estoque:
    get:
      operationId: verificarEstoque
      summary: Verifica disponibilidade de estoque
      description: |
        Endpoint crítico chamado pelo serviço de pedidos durante checkout.
        SLA: p99 < 50ms. Circuit breaker configurado com threshold 5 falhas.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: quantidade
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Informação de estoque
          content:
            application/json:
              schema:
                type: object
                properties:
                  disponivel:
                    type: boolean
                  quantidade_disponivel:
                    type: integer
                  reserva_expira_em:
                    type: string
                    format: date-time
                    nullable: true

components:
  schemas:
    Produto:
      type: object
      required: [id, nome, preco, ativo]
      properties:
        id:
          type: string
          format: uuid
        nome:
          type: string
          maxLength: 255
        descricao:
          type: string
          nullable: true
        preco:
          type: number
          format: decimal
          minimum: 0
        preco_promocional:
          type: number
          format: decimal
          nullable: true
        categoria_id:
          type: string
          format: uuid
        ativo:
          type: boolean
        criado_em:
          type: string
          format: date-time
        atualizado_em:
          type: string
          format: date-time

  responses:
    NaoEncontrado:
      description: Recurso não encontrado
      content:
        application/json:
          schema:
            type: object
            properties:
              erro:
                type: string
                example: "Produto não encontrado"
              codigo:
                type: string
                example: "PRODUTO_NAO_ENCONTRADO"

    ServicoIndisponivel:
      description: Serviço temporariamente indisponível
      headers:
        Retry-After:
          schema:
            type: integer
          description: Segundos até tentar novamente

Modelo de Dados

Cada serviço é dono do seu schema. Os schemas são versionados via migrations com Flyway ou equivalente:

-- services/order-service/migrations/V001__criar_tabelas_pedidos.sql

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE pedidos (
  id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  usuario_id      UUID NOT NULL,
  status          VARCHAR(50) NOT NULL DEFAULT 'rascunho',
  subtotal        DECIMAL(12,2) NOT NULL DEFAULT 0,
  desconto        DECIMAL(12,2) NOT NULL DEFAULT 0,
  frete           DECIMAL(12,2) NOT NULL DEFAULT 0,
  total           DECIMAL(12,2) NOT NULL DEFAULT 0,
  moeda           CHAR(3) NOT NULL DEFAULT 'BRL',
  endereco_entrega JSONB NOT NULL DEFAULT '{}',
  metadata        JSONB NOT NULL DEFAULT '{}',
  criado_em       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  atualizado_em   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  confirmado_em   TIMESTAMPTZ,
  cancelado_em    TIMESTAMPTZ,

  CONSTRAINT status_valido CHECK (
    status IN (
      'rascunho', 'aguardando_pagamento', 'pago',
      'em_separacao', 'enviado', 'entregue', 'cancelado'
    )
  ),
  CONSTRAINT total_positivo CHECK (total >= 0),
  CONSTRAINT subtotal_positivo CHECK (subtotal >= 0)
);

CREATE TABLE itens_pedido (
  id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  pedido_id       UUID NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
  produto_id      UUID NOT NULL,
  -- Snapshot do produto no momento do pedido
  -- Não usa FK para catalog-service — serviços são independentes
  nome_produto    VARCHAR(255) NOT NULL,
  preco_unitario  DECIMAL(12,2) NOT NULL,
  quantidade      INTEGER NOT NULL,
  subtotal        DECIMAL(12,2) NOT NULL,

  CONSTRAINT quantidade_positiva CHECK (quantidade > 0),
  CONSTRAINT preco_positivo CHECK (preco_unitario > 0)
);

CREATE TABLE eventos_pedido (
  id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  pedido_id   UUID NOT NULL REFERENCES pedidos(id),
  tipo        VARCHAR(100) NOT NULL,
  payload     JSONB NOT NULL DEFAULT '{}',
  criado_por  VARCHAR(255),  -- ID do usuário ou 'sistema'
  criado_em   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Índices para as queries mais comuns
CREATE INDEX idx_pedidos_usuario_id
  ON pedidos(usuario_id)
  WHERE status NOT IN ('cancelado');

CREATE INDEX idx_pedidos_status_criado
  ON pedidos(status, criado_em DESC);

CREATE INDEX idx_itens_pedido_pedido_id
  ON itens_pedido(pedido_id);

CREATE INDEX idx_eventos_pedido_id
  ON eventos_pedido(pedido_id, criado_em DESC);

-- Trigger para atualizar atualizado_em automaticamente
CREATE OR REPLACE FUNCTION atualizar_timestamp()
RETURNS TRIGGER AS $$
BEGIN
  NEW.atualizado_em = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_pedidos_atualizado_em
  BEFORE UPDATE ON pedidos
  FOR EACH ROW EXECUTE FUNCTION atualizar_timestamp();

SLOs do Sistema

Os Service Level Objectives definem o que significa "funcionar bem" para cada serviço:

# kubernetes/platform/slos.yaml
# SLOs definidos usando o formato OpenSLO

apiVersion: openslo/v1
kind: SLO
metadata:
  name: api-gateway-disponibilidade
  namespace: producao
spec:
  service: api-gateway
  description: "Disponibilidade do API Gateway — requisições com sucesso"

  budgetingMethod: Occurrences

  objectives:
    - displayName: "99.5% de disponibilidade"
      target: 0.995
      # Considera falha: status 5xx ou timeout
      indicator:
        ratio:
          good:
            metric:
              prometheusMetric:
                query: |
                  sum(rate(http_requests_total{
                    service="api-gateway",
                    status!~"5.."
                  }[5m]))
          total:
            metric:
              prometheusMetric:
                query: |
                  sum(rate(http_requests_total{
                    service="api-gateway"
                  }[5m]))
---
apiVersion: openslo/v1
kind: SLO
metadata:
  name: order-service-latencia
  namespace: producao
spec:
  service: order-service
  description: "Latência do checkout — p99 abaixo de 1 segundo"

  budgetingMethod: Occurrences

  objectives:
    - displayName: "p99 < 1s para checkout"
      target: 0.99
      indicator:
        ratio:
          good:
            metric:
              prometheusMetric:
                query: |
                  sum(rate(http_request_duration_seconds_bucket{
                    service="order-service",
                    endpoint="/checkout",
                    le="1.0"
                  }[5m]))
          total:
            metric:
              prometheusMetric:
                query: |
                  sum(rate(http_request_duration_seconds_count{
                    service="order-service",
                    endpoint="/checkout"
                  }[5m]))

Plano de Implementação

Os próximos quatro artigos implementam o sistema em etapas incrementais, cada uma entregando valor independente:

Artigo — Infraestrutura: Provisionamento completo do cluster EKS, RDS, ElastiCache, SQS e networking com Terraform. Ao final, a infraestrutura está pronta para receber os serviços.

Artigo  — Os Serviços: Implementação dos cinco microsserviços com código funcional, testes, Dockerfiles otimizados e manifestos Kubernetes. Ao final, os serviços rodam localmente com Docker Compose e no cluster com kubectl apply.

Artigo — Pipeline Completo: Pipeline de CI/CD com GitHub Actions que executa testes, scanning de segurança, build de imagem, deploy no EKS via ArgoCD e verificação pós-deploy. Ao final, um push para main resulta automaticamente em deploy em produção.

Artigo — Operações em Produção: Dashboards de observabilidade, alertas calibrados, runbooks, experimentos de chaos, configuração de backups e o postmortem do primeiro incidente simulado. Ao final, o sistema está operacionalmente maduro.

O Que Vem a Seguir

O próximo artigo começa a implementação com o que sustenta tudo: a infraestrutura. O Terraform provisiona o cluster EKS, os bancos de dados, o cache, as filas e toda a rede com os controles de segurança que os artigos anteriores (Resiliência e Chaos Engineering, Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento, Cultura DevOps: Maturidade, Postmortems e Melhoria Contínua) descreveram — desta vez, integrados em um único sistema coerente.

Referências para Aprofundamento

Arquitetura de microsserviços

Architecture Decision Records

  • ADR GitHub — adr.github.io — Repositório de templates e ferramentas para Architecture Decision Records, incluindo o formato MADR e ferramentas de linha de comando para gerenciamento.

OpenSLO

  • OpenSLO Specification — openslo.com — Especificação aberta para definição de SLOs, com suporte a múltiplos backends de observabilidade e integração com Kubernetes via operadores.

Exercícios

Exercício 1

A migration cria este índice:

CREATE INDEX idx_pedidos_usuario_id
  ON pedidos(usuario_id)
  WHERE status NOT IN ('cancelado');

A tela "Meus Pedidos" executa SELECT * FROM pedidos WHERE usuario_id = $1 ORDER BY criado_em DESC e ficou lenta em produção. O EXPLAIN mostra Seq Scan. O índice não deveria resolver exatamente essa consulta?

Ver resposta

✓ Resposta: Não, e a razão é o WHERE na definição: trata-se de um índice parcial. Ele indexa apenas as linhas que satisfazem status NOT IN ('cancelado') — os pedidos cancelados não estão nele.

O planejador só pode usar um índice parcial quando consegue provar, a partir da consulta, que todas as linhas desejadas estão indexadas. A query da tela não menciona status, então ela pede também os cancelados; usar o índice devolveria um resultado incompleto. Sem alternativa, o PostgreSQL varre a tabela inteira.

Há duas saídas, e a escolha diz respeito ao produto, não ao banco. Se a tela realmente não deve listar cancelados, a correção é alinhar a query ao índice — e o resultado fica mais rápido do que seria com um índice completo, porque o índice parcial é menor:

SELECT * FROM pedidos
 WHERE usuario_id = $1
   AND status NOT IN ('cancelado')
 ORDER BY criado_em DESC;

Se os cancelados precisam aparecer, o índice é que está errado para esse caso de uso e deve ser total. Vale notar que a condição da query não precisa ser idêntica à do índice — basta ser logicamente mais restritiva: status = 'entregue' também permite o uso, porque implica status NOT IN ('cancelado').

Índices parciais são uma ótima ferramenta justamente quando a maioria das consultas ignora um subconjunto grande e estável de linhas — o que costuma ser verdade para registros cancelados ou arquivados. O risco é serem invisíveis: quem escreve a query meses depois vê "existe índice em usuario_id" na listagem e não repara na cláusula.

Exercício 2

A tabela itens_pedido guarda nome_produto e preco_unitario, duplicando dados que já existem no serviço de catálogo — e o comentário registra que não há FK porque "serviços são independentes". Além da independência entre serviços, existe uma razão de domínio para essa duplicação. Qual?

Ver resposta

✓ Resposta: Porque o preço de um item vendido é um fato histórico, não uma referência ao preço atual. Mesmo que catálogo e pedidos compartilhassem o mesmo banco, esses campos precisariam existir.

Se itens_pedido apenas apontasse para o produto, uma promoção amanhã reescreveria o valor de um pedido fechado ontem. A nota fiscal deixaria de bater com o pedido, o relatório de faturamento do mês passado mudaria sozinho, e uma devolução seria processada pelo preço errado. O mesmo vale para nome_produto: renomear "Camiseta Azul" para "Camiseta Azul Marinho" não pode reescrever o histórico de quem comprou.

É a distinção entre dado de referência (o produto, que evolui) e dado transacional (o que foi efetivamente vendido, que é imutável). Chamar isso de "desnormalização" atrapalha o entendimento — não é uma otimização com custo de consistência, é a modelagem correta: são duas informações diferentes que por acaso coincidem no instante da compra.

O produto_id continua ali, e é o que permite responder "quais pedidos incluíram este produto" numa investigação ou num recall — mas como ponteiro fraco, sem integridade referencial imposta pelo banco. A consequência a aceitar é que o produto pode ser removido do catálogo e o item do pedido sobreviverá apontando para um id que não resolve mais. Em um sistema distribuído isso é esperado, e é por isso que o snapshot precisa conter tudo que o pedido precisa para se explicar sozinho.

Repare que o mesmo raciocínio aparece em pedidos.endereco_entrega como JSONB, e não como FK para uma tabela de endereços do usuário: o endereço para onde a encomenda foi enviada não muda quando a pessoa se muda.

Exercício 3

Compare as duas chaves estrangeiras da migration:

pedido_id UUID NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,  -- itens_pedido
pedido_id UUID NOT NULL REFERENCES pedidos(id),                    -- eventos_pedido

A diferença é intencional. Explique o efeito de cada uma em um DELETE FROM pedidos WHERE id = '...' — e o que a existência de cancelado_em na tabela sugere sobre a frequência real desse comando.

Ver resposta

✓ Resposta: O DELETE falha. Os itens seriam apagados junto pelo CASCADE, mas eventos_pedido não declara ação de exclusão — e o padrão do PostgreSQL é NO ACTION, que bloqueia a remoção do pai enquanto houver filhos. Como todo pedido gera eventos desde a criação, na prática nenhum pedido pode ser deletado.

A assimetria é coerente com a natureza de cada tabela. Os itens não existem sem o pedido — são parte dele, e sozinhos não significam nada; o CASCADE é correto ali. Os eventos são trilha de auditoria: registram o que aconteceu, e apagá-los junto com o pedido destruiria exatamente o registro que serve para investigar uma fraude, uma cobrança contestada ou um incidente. A FK que bloqueia é o que transforma "não apague pedidos" de convenção em regra imposta pelo banco.

Sobre o cancelado_em: sua presença indica que o modelo trabalha com exclusão lógica. Cancelar um pedido é uma transição de estado — status = 'cancelado' mais o carimbo de tempo — e não uma remoção de linha. O DELETE físico não faz parte do fluxo normal do sistema; se aparecer, é operação manual ou expurgo administrativo, precisamente o momento em que se quer que o banco resista.

Vale o alerta geral: ON DELETE CASCADE é decisão consciente, nunca padrão. Escrito por hábito, ele apaga em silêncio dados que ninguém pretendia remover, e a descoberta acontece semanas depois, quando alguém procura o histórico. A pergunta que decide é sempre a mesma — este registro filho ainda significa alguma coisa sem o pai? Para itens de um pedido, não; para o registro de que algo aconteceu, sim.

Exercício 4

O SLO order-service-latencia promete "p99 < 1s para checkout", mas a query não usa histogram_quantile — ela divide a contagem do bucket le="1.0" pelo total, com target: 0.99. Isso mede a mesma coisa que calcular o p99? Qual abordagem é a correta para um SLO?

Ver resposta

✓ Resposta: Não é a mesma conta, e a do artigo é a correta para um SLO.

histogram_quantile responde "qual foi a latência do percentil 99?" — devolve um valor em segundos, estimado por interpolação linear dentro do bucket onde o percentil cai. A precisão depende inteiramente de quão finos são os buckets naquela faixa, e o número resultante ainda precisa ser comparado com o limite.

A query do artigo responde a pergunta invertida: "que proporção das requisições ficou abaixo de 1 segundo?". O bucket le="1.0" é uma contagem exata — não há interpolação nem estimativa. Se 99% ou mais das requisições estiverem nele, o objetivo foi cumprido. É por isso que o budgetingMethod é Occurrences: o SLO conta eventos bons contra eventos totais, e é essa razão que gera o error budget — com target: 0.995 no gateway, 0,5% das requisições podem falhar no período, e o quanto já foi gasto é diretamente calculável.

A diferença prática aparece no alerta. Um alerta sobre o valor do p99 dispara quando a latência passa do limite agora; um alerta sobre consumo de error budget dispara quando a violação acumulada ameaça o compromisso do trimestre — que é o que de fato importa para quem depende do serviço. Também torna o SLO auditável: um cliente pode conferir a conta, coisa que um percentil interpolado não permite.

Um detalhe operacional decisivo: a query exige que exista um bucket exatamente com le="1.0" no histograma. Buckets do Prometheus são definidos na instrumentação, e o padrão do prom-client vai de 0.005 a 10 sem passar por 1.0 em algumas configurações. Se o bucket não existir, a query não erra — ela retorna vazio, e o SLO simplesmente não mede nada, silenciosamente. O limite do SLO precisa ser escolhido junto com os buckets da instrumentação.

Exercício 5

O ADR-002 define que Pedidos → Catálogo para verificar estoque é síncrono, e que Pedidos → Catálogo para atualizar estoque após confirmação é assíncrono via SQS. Dois clientes fazem checkout do último item em estoque ao mesmo tempo. O que acontece? Que pista o contrato OpenAPI dá sobre a solução pretendida?

Ver resposta

✓ Resposta: Os dois checkouts são aprovados e o produto é vendido duas vezes — o clássico oversell.

A sequência: ambos chamam GET /produtos/{id}/estoque?quantidade=1; o catálogo responde disponivel: true para os dois, porque nada foi decrementado ainda; ambos confirmam o pedido; ambos publicam o evento de baixa no SQS. O decremento acontece depois, de forma assíncrona, e o estoque vai a −1. A verificação síncrona é apenas uma leitura, e leitura não reserva nada — entre o "sim, tem" e o "agora tire um" existe uma janela em que qualquer outro pedido cabe.

A pista está no schema da resposta: reserva_expira_em, do tipo date-time e anulável. O nome revela que a operação pretendida não é consultar, e sim reservar. O padrão correto é a chamada síncrona decrementar atomicamente um contador de estoque disponível e devolver uma reserva com prazo — o segundo cliente recebe disponivel: false imediatamente. Se o pedido for confirmado, a reserva vira baixa definitiva; se o pagamento falhar ou o prazo expirar, a reserva é liberada e o item volta à prateleira.

Isso muda a semântica do endpoint, e o contrato deveria acompanhar: GET precisa ser seguro e sem efeitos colaterais, então uma reserva pertence a um POST /produtos/{id}/reservas. Um GET que altera estado quebra a expectativa de qualquer proxy, cache ou cliente HTTP — e o CloudFront está no caminho.

O ponto mais geral: consistência eventual é aceitável para propagar fatos, não para decidir sobre recursos escassos. O ADR-003 aceita "consistência eventual entre serviços" e isso é adequado para atualizar a vitrine do catálogo depois de uma venda; deixar de ser adequado no exato momento em que duas transações disputam a mesma unidade. A decisão precisa acontecer em um ponto serializador — no caso, o próprio catálogo, com um UPDATE ... SET quantidade = quantidade - 1 WHERE quantidade >= 1 cuja contagem de linhas afetadas decide quem levou.

Comentários

Mais em DevOps

Route53, CloudFront e ACM: Rede e Entrega de Conteúdo
Route53, CloudFront e ACM: Rede e Entrega de Conteúdo

A camada de entrega que fica entre o usuário e a aplicação: hosted zones e…

Gitea: Self-Hosted Leve para Times Menores
Gitea: Self-Hosted Leve para Times Menores

Servidor Git self-hosted em um único binário Go: instalação por systemd ou…

Escrevendo um Dockerfile do Zero
Escrevendo um Dockerfile do Zero

Escrevendo um Dockerfile do zero: o papel de cada instrução, por que a ordem…