⚡ Técnicas Avançadas de Desenvolvimento de Software

Arquitetura  |  Async  |  Cache  |  Filas  |  Microsserviços

Arquitetura de Software
Arquitetura em Camadas (Layered)
Requisição HTTP ↓ Camada de Apresentação (Routes/Controllers) ↓ valida entrada, chama service Camada de Negócio (Services) ↓ lógica de negócio, orquestra Camada de Dados (Repository/DAO) ↓ queries ao banco Banco de Dados
# Exemplo em FastAPI com camadas # routes/usuario.py — apresentação @router.post("/usuarios") def criar(dados: UsuarioSchema, service: UsuarioService = Depends()): return service.criar_usuario(dados) # services/usuario.py — negócio class UsuarioService: def __init__(self, repo: UsuarioRepository = Depends()): self.repo = repo def criar_usuario(self, dados: UsuarioSchema): if self.repo.buscar_por_email(dados.email): raise HTTPException(400, "Email já cadastrado") senha_hash = bcrypt.hash(dados.senha) return self.repo.criar(dados.nome, dados.email, senha_hash) # repositories/usuario.py — dados class UsuarioRepository: def __init__(self, db: Session = Depends(get_db)): self.db = db def criar(self, nome, email, senha_hash): usuario = Usuario(nome=nome, email=email, senha=senha_hash) self.db.add(usuario); self.db.commit() return usuario
Arquiteturas Comuns
ArquiteturaEstruturaVantagemQuando usar
MonolitoTudo em 1 aplicaçãoSimples de desenvolverInício de projetos, times pequenos
MicrosserviçosServiços independentesEscala e deploy independentesTimes grandes, alta escala
ServerlessFunções sob demandaZero gerenciamento de servidorEventos, ETL, automações
Event-DrivenComunicação via eventosBaixo acoplamentoIntegração entre sistemas
⚠ Monolito primeiro!
Comece com monolito bem estruturado. Microsserviços adicionam complexidade enorme (rede, deploy, observabilidade). Só migre quando o monolito se tornar um problema real — não antes.
SOLID na Prática em Python
# S — Single Responsibility # Ruim: uma classe faz tudo class UsuarioManager: def criar(self): ... def enviar_email(self): ... # responsabilidade extra! def salvar_log(self): ... # responsabilidade extra! # Bom: cada classe faz uma coisa class UsuarioService: def criar(self): ... class EmailService: def enviar(self): ... class LogService: def registrar(self): ... # D — Dependency Inversion (injeta abstração, não implementação) from abc import ABC, abstractmethod class NotificacaoBase(ABC): @abstractmethod def enviar(self, msg: str): ... class EmailNotificacao(NotificacaoBase): def enviar(self, msg): print(f"Email: {msg}") class SMSNotificacao(NotificacaoBase): def enviar(self, msg): print(f"SMS: {msg}") class PedidoService: def __init__(self, notificacao: NotificacaoBase): self.notif = notificacao # depende da abstração def finalizar(self, pedido): self.notif.enviar(f"Pedido {pedido.id} confirmado!")
Exercício 1: Por que separar Service de Repository?
→ Service cuida da lógica de negócio. Repository cuida do acesso aos dados. São responsabilidades diferentes.
Se trocar PostgreSQL por MongoDB, só muda o Repository — o Service não sabe do banco. Se mudar a regra de negócio, só muda o Service — o banco não sabe. Isso é o princípio de responsabilidade única aplicado em camadas.
Programação Assíncrona e Performance
Async/Await em Python

Python síncrono: enquanto espera o banco responder, a thread fica bloqueada. Python assíncrono: enquanto espera o banco, processa outra requisição. Ideal para APIs com I/O intenso.

import asyncio import httpx # cliente HTTP async # Síncrono — bloqueia enquanto espera def buscar_usuario_sync(id): resultado = db.query(...) # bloqueia a thread return resultado # Assíncrono — libera a thread enquanto espera async def buscar_usuario(id): resultado = await db.fetch(...) # libera a thread return resultado # FastAPI suporta async nativamente @app.get("/usuarios/{id}") async def get_usuario(id: int): usuario = await usuario_service.buscar(id) return usuario # Executar múltiplas operações em paralelo async def dados_dashboard(user_id: int): # executa as 3 queries ao mesmo tempo! pedidos, saldo, notifs = await asyncio.gather( buscar_pedidos(user_id), buscar_saldo(user_id), buscar_notificacoes(user_id) ) return {"pedidos": pedidos, "saldo": saldo, "notifs": notifs}
⚠ Quando usar async
Use async para operações de I/O: banco de dados, APIs externas, leitura de arquivos. Para operações CPU-intensivas (processamento de imagem, ML), async não ajuda — use multiprocessing ou workers separados.
Otimização de Queries — N+1 Problem
# PROBLEMA N+1 — faz 1 query + N queries (uma por usuário) usuarios = db.query(Usuario).all() for u in usuarios: print(u.pedidos) # gera 1 query por usuario! # SOLUÇÃO — eager loading (1 query com JOIN) usuarios = db.query(Usuario).options( joinedload(Usuario.pedidos) ).all() # Agora: 1 query só # Paginação — nunca retorne tudo! @app.get("/usuarios") async def listar(pagina: int = 1, limite: int = 20): offset = (pagina - 1) * limite return db.query(Usuario).offset(offset).limit(limite).all() # Índices — essencial para queries rápidas # No SQLAlchemy: class Usuario(Base): email = Column(String, index=True) # cria índice automaticamente
Exercício 2: O que é o problema N+1 e como resolver?
→ N+1 ocorre quando se faz 1 query para listar + N queries para buscar detalhes de cada item. Resolver com eager loading (JOIN na query principal).
100 usuários com pedidos = 1 + 100 = 101 queries. Com joinedload = 1 query com JOIN. A diferença pode ser de milissegundos vs segundos em produção.
Cache e Filas de Mensagens
Cache com Redis

Cache armazena resultados computados para evitar reprocessamento. Redis é o cache mais usado — in-memory, extremamente rápido (sub-milissegundo).

pip install redis import redis import json r = redis.Redis(host='localhost', port=6379, db=0) # Cache simples def buscar_produto(produto_id: int): cache_key = f"produto:{produto_id}" # Tentar do cache primeiro cached = r.get(cache_key) if cached: return json.loads(cached) # cache hit! # Cache miss — busca do banco produto = db.query(Produto).get(produto_id) if produto: # Salva no cache por 5 minutos r.setex(cache_key, 300, json.dumps(produto.dict())) return produto # Decorator de cache (mais elegante) from functools import wraps def cache(expira_em=300): def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): key = f"{func.__name__}:{args}:{kwargs}" cached = r.get(key) if cached: return json.loads(cached) resultado = await func(*args, **kwargs) r.setex(key, expira_em, json.dumps(resultado)) return resultado return wrapper return decorator @cache(expira_em=600) async def ranking_produtos(): return await db.fetch("SELECT * FROM produtos ORDER BY vendas DESC LIMIT 10")
Filas com Celery + Redis

Filas de mensagens processam tarefas pesadas em background sem bloquear a resposta da API. Ex: enviar email, gerar PDF, processar imagem — o usuário não espera.

pip install celery redis # celery_app.py from celery import Celery app = Celery('tasks', broker='redis://localhost:6379/0') @app.task def enviar_email(destinatario: str, assunto: str, corpo: str): # lógica de envio de email smtp.send(destinatario, assunto, corpo) print(f"Email enviado para {destinatario}") @app.task def gerar_relatorio(usuario_id: int): dados = buscar_dados(usuario_id) pdf = criar_pdf(dados) salvar_s3(pdf) # Na API — não bloqueia a resposta! @app.post("/usuarios/{id}/relatorio") async def solicitar_relatorio(id: int): gerar_relatorio.delay(id) # enfileira a tarefa return {"mensagem": "Relatório sendo gerado, você receberá por email"} # Rodar worker em outro terminal: # celery -A celery_app worker --loglevel=info
TecnologiaUsoQuando usar
Redis (cache)Armazenar resultados temporáriosQueries lentas, dados que não mudam muito
Celery + RedisProcessar tarefas em backgroundEmail, PDF, processamento de imagem
RabbitMQFila de mensagens robustaIntegração entre sistemas, alta confiabilidade
KafkaStream de eventos em alta escalaLogs, analytics em tempo real, big data
Exercício 3: Por que usar fila para envio de email e não fazer direto na API?
→ Envio de email é lento (rede, SMTP) e pode falhar. Com fila, a API responde imediatamente e o email é enviado em background com retry automático.
Sem fila: usuário espera 2-3 segundos pela resposta da API enquanto o email é enviado. Se falhar, o usuário recebe erro. Com Celery: API responde em ms, Celery tenta enviar o email e, se falhar, tenta novamente automaticamente.
Microsserviços e Comunicação
Quando migrar para Microsserviços
Sinais que o monolito está no limite
Deploy demora muito e afeta tudo.
Times diferentes editam os mesmos arquivos.
Um módulo lento afeta toda a aplicação.
Precisam de tecnologias diferentes por módulo.
Benefícios dos microsserviços
Deploy independente por serviço.
Escala só o que precisa (ex: só o serviço de busca).
Times autônomos com tecnologias diferentes.
Falha isolada — um serviço cai sem derrubar tudo.
Comunicação entre Microsserviços
TipoComoQuando usarProblema
Síncrona (REST/gRPC)Serviço A chama API do B e esperaResultado imediato necessárioAcoplamento, se B cair, A falha
Assíncrona (Mensagens)Publica evento na fila, B consome depoisOperações que podem ser atrasadasConsistência eventual
# API Gateway — ponto de entrada único # Nginx como gateway simples upstream servico_usuarios { server usuarios-api:8001; } upstream servico_pedidos { server pedidos-api:8002; } server { location /api/usuarios { proxy_pass http://servico_usuarios; } location /api/pedidos { proxy_pass http://servico_pedidos; } } # gRPC — comunicação eficiente entre serviços internos # Proto file: usuario.proto syntax = "proto3"; service UsuarioService { rpc BuscarUsuario (UsuarioRequest) returns (UsuarioResponse); } message UsuarioRequest { int32 id = 1; } message UsuarioResponse { string nome = 1; string email = 2; }
Observabilidade — Saber o que está acontecendo
import logging import time from opentelemetry import trace # Logging estruturado logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s') logger = logging.getLogger(__name__) @app.middleware("http") async def log_requests(request, call_next): inicio = time.time() response = await call_next(request) duracao = time.time() - inicio logger.info(f"method={request.method} path={request.url.path} " f"status={response.status_code} duration={duracao:.3f}s") return response # Métricas com Prometheus from prometheus_client import Counter, Histogram requisicoes = Counter('http_requests_total', 'Total de requisições', ['method', 'path']) latencia = Histogram('http_request_duration_seconds', 'Latência das requisições')
⚠ Os 3 pilares de Observabilidade
Logs: o que aconteceu (eventos). Métricas: quantas vezes, quanto tempo (números). Traces: o caminho de uma requisição entre serviços. Ferramentas: ELK Stack, Prometheus+Grafana, Jaeger.
📝 Cola — Técnicas Avançadas
Arquitetura em Camadas
Routes → Services → Repositories → Database (entrada) (negócio) (dados) (persistência)
Async vs Sync
SituaçãoUse
I/O: banco, API externa, arquivoasync/await
CPU: ML, processamento de imagemmultiprocessing / worker
Múltiplas operações independentesasyncio.gather()
⭐ Regras de Ouro
Regra
Comece com monolito bem estruturado — microsserviços são complexidade, não bala de prata
Regra
N+1: sempre use eager loading em relações. Nunca query dentro de loop
Regra
Cache: leia do Redis primeiro. Se miss, busca do banco e salva no cache
RegraTarefas lentas (email, PDF) → fila Celery. API responde imediatamente
RegraService não conhece o banco. Repository não conhece regra de negócio
Regra
Observabilidade: logs + métricas + traces — sem isso você voa cego em produção