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.
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-serviceeorder-servicecom/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
- 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;
}
}
- 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);
}
}
- 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
- 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
- Subir serviços:
docker compose -f infra/docker-compose.yml up --build -d customer-service order-service
- 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.
- 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"}
- 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-serviceao banco de clientes. - Criar endpoint sem validação de payload.
- Retornar
500para 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.