Design patterns para escalabilidade

1. Fundamentos da Escalabilidade com Design Patterns

Escalabilidade é a capacidade de um sistema lidar com cargas crescentes sem degradação de desempenho. Existem duas abordagens principais: escalabilidade vertical (adicionar recursos a uma única máquina) e escalabilidade horizontal (adicionar mais instâncias). Design patterns específicos são essenciais para viabilizar a escalabilidade horizontal, que é a abordagem mais resiliente e econômica em nuvem.

Os patterns clássicos do GoF (Gang of Four) foram criados para sistemas monolíticos e não abordam problemas de distribuição, concorrência ou tolerância a falhas em larga escala. Para escalabilidade moderna, precisamos de patterns que respeitem princípios como GRASP (baixo acoplamento, alta coesão) e YAGNI (não adicionar complexidade desnecessária). A escolha correta de patterns deve equilibrar o custo da implementação com o ganho real de escala.

2. Padrões de Particionamento e Distribuição

Sharding Pattern

Divide dados em partições independentes (shards) para distribuir carga de escrita e leitura.

# Exemplo de sharding por ID de usuário
def get_shard(user_id):
    shard_count = 4
    shard_id = user_id % shard_count
    return f"db_shard_{shard_id}"

# Roteamento de consulta
def save_user(user):
    shard = get_shard(user.id)
    shard.save(user)

CQRS (Command Query Responsibility Segregation)

Separa operações de escrita (commands) e leitura (queries) em modelos distintos.

# Command: escreve no banco principal
class CreateOrderCommand:
    def execute(order_data):
        db_primary.orders.insert(order_data)
        event_bus.publish("order_created", order_data)

# Query: lê de uma réplica otimizada
class GetOrderQuery:
    def execute(order_id):
        return db_read_replica.orders.find_by_id(order_id)

Event Sourcing

Persiste eventos como fonte única de verdade, permitindo reconstrução de estado e escalabilidade assíncrona.

# Evento armazenado
event = {
    "type": "ORDER_CREATED",
    "data": {"order_id": 123, "items": ["book"]},
    "timestamp": "2024-01-01T10:00:00Z"
}
event_store.append(event)

3. Padrões de Cache e Aceleração

Cache-Aside

A aplicação gerencia o cache manualmente, consultando-o antes do banco.

def get_user(user_id):
    user = cache.get(f"user:{user_id}")
    if not user:
        user = db.users.find_by_id(user_id)
        cache.set(f"user:{user_id}", user, ttl=3600)
    return user

Read-Through / Write-Through

O cache atua como proxy transparente para o banco de dados.

# Read-Through: cache carrega automaticamente
user = cache.load("user:123", lambda: db.users.find(123))

# Write-Through: cache atualiza banco
cache.set("user:123", new_data, write_through=True)

Cache Invalidation

Estratégias para manter consistência: TTL, notificação por eventos e write-behind.

# TTL simples
cache.set("product:456", data, ttl=300)

# Invalidação por evento
def on_product_updated(event):
    cache.delete(f"product:{event.product_id}")

4. Padrões de Desacoplamento e Resiliência

Queue-Based Load Leveling

Usa filas para suavizar picos de requisição, processando em ritmo controlado.

# Produtor envia para fila
queue.send("order_queue", order_data)

# Consumidor processa em lotes
def process_orders():
    batch = queue.receive("order_queue", max_messages=10)
    for order in batch:
        process_order(order)

Circuit Breaker

Protege contra falhas em cascata em sistemas distribuídos.

circuit = CircuitBreaker(failure_threshold=5, recovery_timeout=30)

def call_external_service():
    if circuit.is_open():
        return fallback_response()
    try:
        result = external_api.call()
        circuit.record_success()
        return result
    except Exception:
        circuit.record_failure()
        raise

Bulkhead

Isola recursos por pool ou thread para evitar colapso total.

# Pools separados para diferentes serviços
pool_a = ThreadPoolExecutor(max_workers=10)  # Serviço crítico
pool_b = ThreadPoolExecutor(max_workers=3)   # Serviço não crítico

pool_a.submit(critical_task)
pool_b.submit(non_critical_task)

5. Padrões de Processamento Assíncrono

Event-Driven Architecture

Mensageria e streams para escalar processamento de forma desacoplada.

# Publicador de eventos
event_bus.publish("user.registered", {"user_id": 123})

# Consumidor assíncrono
@event_handler("user.registered")
def send_welcome_email(event):
    email_service.send(event.data["user_id"])

Saga Pattern

Coordena transações distribuídas sem bloqueio, com compensações em caso de falha.

class OrderSaga:
    def execute(self):
        step1 = reserve_inventory(order)
        if step1.failed:
            return compensate_all()
        step2 = charge_payment(order)
        if step2.failed:
            compensate_step1()
            return
        step3 = confirm_order(order)
        saga_complete()

Outbox Pattern

Garante entrega de eventos sem perda de consistência.

# Transação atômica: salva dados + evento
def create_order(order):
    with db.transaction():
        db.orders.insert(order)
        db.outbox.insert({"type": "order_created", "data": order})

# Worker processa outbox
def process_outbox():
    events = db.outbox.select_pending()
    for event in events:
        message_queue.send(event)
        db.outbox.mark_sent(event.id)

6. Padrões de Escalabilidade em Nível de Aplicação

Stateless Services

Remove estado da aplicação para escalar horizontalmente.

# Serviço sem estado: sessão armazenada externamente
def handle_request(request):
    session_data = redis.get(request.session_id)
    result = process(session_data, request.body)
    return result

Sidecar Pattern

Componentes auxiliares (logs, monitoria) sem acoplar ao core.

# Sidecar para logging
sidecar:
  - name: log-collector
    image: fluentd
    volumeMounts:
      - mountPath: /var/log/app
        name: app-logs

Throttling e Rate Limiting

Controla uso para evitar sobrecarga.

# Rate limiter por IP
limiter = RateLimiter(max_requests=100, window_seconds=60)

def api_endpoint(request):
    if not limiter.allow(request.ip):
        return HTTP 429 Too Many Requests
    return process_request(request)

7. Padrões de Infraestrutura e Observabilidade

Service Discovery

Registro e descoberta dinâmica de instâncias.

# Registro automático
service_registry.register("user-service", "192.168.1.10:8080")

# Descoberta
instances = service_registry.discover("order-service")
target = instances[0]  # Load balancing simples

Health Check Patterns

Readiness e liveness probes para orquestração.

# Readiness: serviço pronto para receber tráfego
@app.route("/health/ready")
def readiness():
    if db.is_connected() and cache.is_connected():
        return "OK", 200
    return "Not Ready", 503

# Liveness: serviço ainda vivo
@app.route("/health/live")
def liveness():
    return "OK", 200

Distributed Tracing

Rastreamento de requisições entre serviços para debug de gargalos.

# Inicia trace
trace_id = generate_trace_id()
span = tracer.start_span("process_order", trace_id=trace_id)

# Propagação entre serviços
headers = {"trace_id": trace_id}
response = http_client.get("http://payment-service", headers=headers)

Referências