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
- Node.js Best Practices - Projeto Escalável — Guia abrangente com boas práticas para estruturação de projetos Node.js escaláveis, incluindo organização de diretórios e padrões arquiteturais.
- Microsoft - Padrões de Design para Microsserviços — Documentação oficial da Microsoft sobre padrões de design para arquiteturas de microsserviços escaláveis.
- Martin Fowler - Patterns of Enterprise Application Architecture — Catálogo completo de padrões arquiteturais para aplicações empresariais, incluindo Repository, Unit of Work e Service Layer.
- Redis Documentation - Caching Patterns — Documentação oficial do Redis com padrões de cache para melhorar performance e escalabilidade de aplicações backend.
- OpenTelemetry - Distributed Tracing — Documentação oficial sobre implementação de tracing distribuído para monitoramento de requisições em sistemas de microsserviços.
- K6 - Testes de Carga — Ferramenta open-source para testes de carga e stress, essencial para validar a escalabilidade de projetos backend.
- AWS Well-Architected Framework - Escalabilidade — Guia da AWS com princípios e melhores práticas para arquiteturas escaláveis na nuvem.