Como estruturar um projeto backend escalável

1. Fundamentos da Arquitetura Escalável

1.1. Princípios de design: desacoplamento, statelessness e idempotência

Projetos backend escaláveis começam com três princípios fundamentais. O desacoplamento garante que componentes possam evoluir independentemente. A statelessness permite que qualquer servidor atenda qualquer requisição, facilitando a escalabilidade horizontal. A idempotência assegura que requisições repetidas produzam o mesmo resultado, essencial para sistemas distribuídos.

# Exemplo de endpoint idempotente
POST /api/pagamentos
Header: Idempotency-Key: uuid-unico
Body: { "valor": 100, "conta": "123" }

# Resposta para primeira chamada
201 Created: { "status": "processado", "id": "pag-001" }

# Resposta para mesma chamada com mesma chave
200 OK: { "status": "ja_processado", "id": "pag-001" }

1.2. Padrões arquiteturais

A escolha entre monolito, microsserviços e modular monolito depende do contexto do projeto. O modular monolito oferece um equilíbrio interessante: organização em módulos independentes dentro de um único processo, facilitando a migração futura para microsserviços.

# Estrutura de modular monolito
src/
  modulo-usuario/
    controllers/
    servicos/
    repositorios/
    modelos/
  modulo-pagamento/
    controllers/
    servicos/
    repositorios/
    modelos/
  modulo-notificacao/
    ...
  infraestrutura/
    banco/
    cache/
    fila/

1.3. Estratégias de escalabilidade

A escalabilidade horizontal (adicionar mais servidores) é preferível à vertical (aumentar recursos de um servidor). Implemente balanceamento de carga com algoritmos como round-robin ou least connections.

2. Organização de Diretórios e Módulos

2.1. Estrutura baseada em domínios (DDD)

Domain-Driven Design organiza o código em torno do negócio, não da tecnologia. Cada domínio contém suas próprias regras, entidades e serviços.

dominios/
  pedidos/
    entidades/
      Pedido.ts
      ItemPedido.ts
    servicos/
      CalculadoraFrete.ts
    repositorios/
      PedidoRepository.ts
    eventos/
      PedidoCriado.ts
  estoque/
    entidades/
      Produto.ts
    servicos/
      ValidadorEstoque.ts
    repositorios/
      ProdutoRepository.ts

2.2. Separação clara entre camadas

Controllers lidam com HTTP, serviços com lógica de negócio, repositórios com persistência. Essa separação permite testar cada camada isoladamente.

# Controller
class UsuarioController {
  constructor(usuarioService) {}

  async criar(req, res) {
    const usuario = await this.usuarioService.criar(req.body)
    res.status(201).json(usuario)
  }
}

# Service
class UsuarioService {
  constructor(usuarioRepository, emailService) {}

  async criar(dados) {
    const usuario = new Usuario(dados)
    await this.usuarioRepository.salvar(usuario)
    await this.emailService.enviarBoasVindas(usuario)
    return usuario
  }
}

# Repository
class UsuarioRepository {
  constructor(db) {}

  async salvar(usuario) {
    return this.db.usuarios.insert(usuario)
  }
}

2.3. Injeção de dependências

Utilize contêineres de DI para gerenciar dependências e facilitar testes.

// container.js
const container = {
  usuarioRepository: new UsuarioRepository(db),
  emailService: new EmailService(config.email),
  usuarioService: new UsuarioService(
    container.usuarioRepository,
    container.emailService
  ),
  usuarioController: new UsuarioController(container.usuarioService)
}

3. Camada de Dados e Persistência

3.1. Bancos relacionais vs NoSQL

Bancos relacionais (PostgreSQL, MySQL) oferecem consistência e transações. NoSQL (MongoDB, Cassandra) oferece escalabilidade horizontal e flexibilidade de esquema. Considere CQRS para separar leituras de escritas.

3.2. Padrões de acesso a dados

O padrão Repository abstrai a lógica de persistência. Unit of Work agrupa operações em transações.

// Repository pattern
class PedidoRepository {
  async buscarPorId(id) {
    return this.db.query('SELECT * FROM pedidos WHERE id = $1', [id])
  }

  async salvar(pedido) {
    const { id, ...dados } = pedido
    return this.db.query(
      'INSERT INTO pedidos (id, cliente, total) VALUES ($1, $2, $3)',
      [id, dados.cliente, dados.total]
    )
  }
}

3.3. Estratégias de cache

Implemente cache em múltiplas camadas: Redis para dados frequentes, CDN para conteúdo estático, cache de consultas no banco.

// Estratégia de cache com Redis
async buscarProduto(id) {
  const cacheKey = `produto:${id}`
  const cached = await redis.get(cacheKey)

  if (cached) return JSON.parse(cached)

  const produto = await db.produtos.findById(id)
  await redis.setex(cacheKey, 3600, JSON.stringify(produto))

  return produto
}

4. Comunicação entre Serviços e APIs

4.1. Design de APIs RESTful

Versionamento na URL ou header, paginação com cursor ou offset, filtros consistentes.

# API versionada com paginação
GET /api/v2/pedidos?cursor=abc123&limit=20&status=ativo

Response:
{
  "data": [...],
  "pagination": {
    "nextCursor": "def456",
    "hasMore": true
  }
}

4.2. Comunicação assíncrona

Filas como RabbitMQ ou Kafka garantem resiliência e desacoplamento.

# Publicando evento
await messageQueue.publish('pedido.criado', {
  pedidoId: '123',
  clienteId: '456',
  total: 150.00
})

# Consumindo evento
messageQueue.subscribe('pedido.criado', async (evento) => {
  await servicoEstoque.atualizar(evento.pedidoId)
  await servicoNotificacao.enviar(evento.clienteId)
})

4.3. Tratamento de falhas

Implemente circuit breaker, retry com backoff exponencial e fallback.

// Circuit breaker pattern
const circuitBreaker = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30000
})

async function chamarServicoExterno() {
  if (circuitBreaker.isOpen()) {
    return fallbackResponse()
  }

  try {
    const resultado = await servicoExterno.chamar()
    circuitBreaker.onSuccess()
    return resultado
  } catch (error) {
    circuitBreaker.onFailure()
    throw error
  }
}

5. Gerenciamento de Configuração e Ambiente

5.1. Variáveis de ambiente e configuração

Utilize arquivos .env para desenvolvimento e sistemas centralizados para produção.

# .env
DATABASE_URL=postgresql://localhost:5432/app
REDIS_URL=redis://localhost:6379
JWT_SECRET=minha-chave-secreta
LOG_LEVEL=debug

# config.js
module.exports = {
  database: {
    url: process.env.DATABASE_URL,
    poolSize: parseInt(process.env.DB_POOL_SIZE || '10')
  },
  redis: {
    url: process.env.REDIS_URL,
    ttl: parseInt(process.env.CACHE_TTL || '3600')
  }
}

5.2. Configuração centralizada

Ferramentas como Consul ou etcd permitem atualizar configurações sem redeploy.

5.3. Feature flags

Implemente toggles para ativar/desativar funcionalidades em produção.

// Feature flag system
const features = {
  novoCheckout: process.env.FEATURE_NOVO_CHECKOUT === 'true',
  relatoriosV2: process.env.FEATURE_RELATORIOS_V2 === 'true'
}

if (features.novoCheckout) {
  return novoCheckoutController(req, res)
} else {
  return checkoutLegadoController(req, res)
}

6. Observabilidade e Monitoramento

6.1. Logging estruturado

Utilize formato JSON para facilitar agregação em ELK ou Loki.

// Log estruturado
logger.info({
  event: 'pedido.criado',
  pedidoId: '123',
  clienteId: '456',
  total: 150.00,
  timestamp: new Date().toISOString(),
  correlationId: req.headers['x-correlation-id']
})

6.2. Métricas de desempenho

Exporte métricas para Prometheus e visualize no Grafana.

// Métricas customizadas
const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total de requisições HTTP',
  labelNames: ['method', 'route', 'status']
})

const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duração das requisições HTTP',
  labelNames: ['method', 'route']
})

6.3. Distributed tracing

Implemente tracing com OpenTelemetry para rastrear requisições entre serviços.

7. Testes e Garantia de Qualidade

7.1. Pirâmide de testes

Priorize testes unitários, seguidos de integração e poucos testes end-to-end.

// Teste unitário
describe('UsuarioService.criar', () => {
  it('deve criar usuario com dados validos', async () => {
    const repo = new UsuarioRepositoryMock()
    const email = new EmailServiceMock()
    const service = new UsuarioService(repo, email)

    const usuario = await service.criar({
      nome: 'João',
      email: 'joao@email.com'
    })

    expect(usuario.nome).toBe('João')
    expect(repo.salvar).toHaveBeenCalled()
    expect(email.enviarBoasVindas).toHaveBeenCalled()
  })
})

7.2. Testes de carga

Utilize k6 ou Artillery para validar escalabilidade.

7.3. CI/CD pipeline

Automatize testes e deploy com GitHub Actions ou Jenkins.

8. Segurança e Performance

8.1. Autenticação e autorização

JWT para autenticação stateless, OAuth2 para delegação de acesso.

8.2. Rate limiting

Proteja APIs contra abuso com limitadores por IP e rota.

// Rate limiting middleware
const rateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // limite por IP
  message: 'Muitas requisições, tente novamente mais tarde'
})

app.use('/api/', rateLimiter)

8.3. Otimizações de performance

Implemente connection pooling para banco de dados, compressão de respostas, lazy loading para dados relacionados.

// Connection pool
const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000
})

// Compressão de resposta
app.use(compression())

// Lazy loading
async buscarPedidoComItens(id) {
  const pedido = await pedidoRepository.buscarPorId(id)
  pedido.itens = await itemRepository.buscarPorPedido(id)
  return pedido
}

Referências