APIs REST e Bounded Contexts

Implementando Customer e Order Services

Você implementa CRUD real de cliente e pedido com validação e organização em camadas.

Avançado 60 min 35 pontos Leitura 0%

Nesta aula você vai

  • Criar endpoints de criação e consulta para clientes e pedidos
  • Aplicar validação de entrada em Spring Boot e FastAPI
  • Persistir dados em memória com contrato estável de API

Implementando Customer e Order Services

Objetivos

  • Criar endpoints de criação e consulta para clientes e pedidos
  • Aplicar validação de entrada em Spring Boot e FastAPI
  • Persistir dados em memória com contrato estável de API

Pré-requisitos

  • Aulas de bounded context e camadas concluídas
  • customer-service e order-service com /health
  • Docker Compose operacional

Conceito

customer-service e order-service são o coração do domínio transacional. Se esses serviços nascerem com contrato frágil, qualquer integração futura (Kafka, pagamentos, notificações) ficará instável. Por isso, a prioridade é ter APIs previsíveis e validação consistente.

Aqui você implementa CRUD inicial sem banco externo, focando na arquitetura e no comportamento HTTP correto. Persistência em memória é suficiente nesta etapa, desde que esteja encapsulada e fácil de trocar depois.

A principal meta didática é ensinar fluxo completo: requisição -> validação -> regra -> persistência -> resposta. Esse padrão evita "atalhos" que costumam custar caro em produção.

Estrutura de arquivos

services/
  customer-service/src/main/java/com/aprendi/customer/
    controller/CustomerController.java
    service/CustomerService.java
    repository/CustomerRepository.java
  order-service/app/
    main.py
    api/orders.py
    service/order_service.py
    repository/order_repository.py

Passo a passo

  1. Criar repositório e service de cliente no Spring
// CustomerRepository.java
package com.aprendi.customer.repository;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;

@Repository
public class CustomerRepository {
  private final Map<String, Map<String, String>> store = new ConcurrentHashMap<>();

  public void save(String id, String fullName, String email) {
    store.put(id, Map.of("id", id, "fullName", fullName, "email", email));
  }

  public Map<String, String> getById(String id) {
    return store.get(id);
  }
}
// CustomerService.java
package com.aprendi.customer.service;

import com.aprendi.customer.repository.CustomerRepository;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
  private final CustomerRepository repository;

  public CustomerService(CustomerRepository repository) {
    this.repository = repository;
  }

  public Map<String, String> create(String fullName, String email) {
    if (fullName == null || fullName.isBlank()) {
      throw new IllegalArgumentException("fullName é obrigatório");
    }
    if (email == null || !email.contains("@")) {
      throw new IllegalArgumentException("email inválido");
    }
    String id = UUID.randomUUID().toString();
    repository.save(id, fullName, email);
    return Map.of("id", id, "fullName", fullName, "email", email);
  }

  public Map<String, String> getById(String id) {
    Map<String, String> customer = repository.getById(id);
    if (customer == null) {
      throw new IllegalArgumentException("cliente não encontrado");
    }
    return customer;
  }
}
  1. Criar controller de cliente
// CustomerController.java
package com.aprendi.customer.controller;

import com.aprendi.customer.service.CustomerService;
import java.util.Map;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/customers")
public class CustomerController {
  private final CustomerService service;

  public CustomerController(CustomerService service) {
    this.service = service;
  }

  @PostMapping
  public Map<String, String> create(@RequestBody Map<String, String> body) {
    return service.create(body.get("fullName"), body.get("email"));
  }

  @GetMapping("/{id}")
  public Map<String, String> getById(@PathVariable String id) {
    return service.getById(id);
  }
}
  1. Criar repositório, service e rotas de pedido no FastAPI
# app/repository/order_repository.py
class OrderRepository:
    def __init__(self):
        self._store: dict[str, dict] = {}

    def save(self, order: dict) -> None:
        self._store[order["id"]] = order

    def get(self, order_id: str) -> dict | None:
        return self._store.get(order_id)
# app/service/order_service.py
import uuid
from app.repository.order_repository import OrderRepository

class OrderService:
    def __init__(self):
        self.repo = OrderRepository()

    def create(self, customer_id: str, total_amount: float):
        order = {
            "id": str(uuid.uuid4()),
            "customer_id": customer_id,
            "total_amount": total_amount,
            "status": "PENDING_PAYMENT",
        }
        self.repo.save(order)
        return order

    def get_by_id(self, order_id: str):
        return self.repo.get(order_id)
# app/api/orders.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from app.service.order_service import OrderService

router = APIRouter(prefix="/orders", tags=["orders"])
service = OrderService()

class CreateOrderIn(BaseModel):
    customer_id: str = Field(min_length=1)
    total_amount: float = Field(gt=0)

@router.post("")
def create_order(payload: CreateOrderIn):
    return service.create(payload.customer_id, payload.total_amount)

@router.get("/{order_id}")
def get_order(order_id: str):
    order = service.get_by_id(order_id)
    if not order:
        raise HTTPException(status_code=404, detail="pedido não encontrado")
    return order
  1. Conectar o router de pedidos no main.py

Substitua services/order-service/app/main.py por:

from fastapi import FastAPI
from app.api.orders import router as orders_router

app = FastAPI()
app.include_router(orders_router)

@app.get("/health")
def health():
    return {"status": "ok", "service": "order-service"}

Como testar

  1. Subir serviços:
docker compose -f infra/docker-compose.yml up --build -d customer-service order-service
  1. Criar cliente:
curl -s -X POST http://localhost:8081/customers \
  -H "Content-Type: application/json" \
  -d '{"fullName":"Bruno Costa","email":"bruno@aprendi.dev"}'

Saída esperada: objeto com id, fullName, email.

  1. Criar pedido:
curl -s -X POST http://localhost:8000/orders \
  -H "Content-Type: application/json" \
  -d '{"customer_id":"cli-123","total_amount":199.90}'

Saída esperada:

{"id":"...","customer_id":"cli-123","total_amount":199.9,"status":"PENDING_PAYMENT"}
  1. Consultar pedido:
curl -s http://localhost:8000/orders/<ORDER_ID>

Dicas de projeto

  • Retorne IDs imutáveis na criação para facilitar rastreio.
  • Valide o payload no início do fluxo.
  • Evite expor detalhes internos de persistência.
  • Mantenha status de pedido explícito (PENDING_PAYMENT, etc.).

Erros comuns

  • Acoplar order-service ao banco de clientes.
  • Criar endpoint sem validação de payload.
  • Retornar 500 para erro de domínio simples.
  • Misturar lógica de negócio dentro da rota HTTP.

Resumo

Você implementou APIs reais de cliente e pedido com validação, organização em camadas e comportamento HTTP previsível. Essa base é o pré-requisito para começar publicação de eventos com segurança.