Pular para conteúdo

System Design

Projetar um sistema é decidir como ele será organizado e quais garantias vai dar além de "funcionar": quão rápido responde, quanta carga aguenta, quanto tempo fica no ar, quão protegido está. Essas garantias são os requisitos não funcionais (ou atributos de qualidade). Esta página é o mapa das ferramentas para atingi-los e dos custos de cada escolha — a base de qualquer conversa de system design em entrevista.

Definição: arquitetura de software

Estrutura fundamental de um sistema: seus componentes, as relações entre eles e com o ambiente e os princípios que guiam seu projeto e evolução (definição do padrão IEEE 1471). Na prática, inclui também decisões como linguagem, provedor de nuvem e o equilíbrio entre os requisitos não funcionais.

Requisitos funcionais x não funcionais

Os requisitos funcionais dizem o que o sistema faz (cadastrar usuário, calcular frete). Os não funcionais dizem quão bem ele faz. Um sistema que cumpre todas as funções, mas demora minutos para responder ou cai toda sexta-feira, é um mau sistema.

Atributo Pergunta que responde
Desempenho O tempo de resposta é aceitável para o usuário?
Escalabilidade Continua bom se o número de usuários ou de dados crescer muito?
Elasticidade Consegue aumentar e diminuir recursos sozinho conforme a demanda varia?
Confiabilidade / disponibilidade Responde sempre, ou quase sempre?
Tolerância a falhas e recuperabilidade Quando algo quebra (e vai quebrar), degrada com elegância e volta rápido?
Segurança Só quem deve acessa; os dados estão protegidos e íntegros?
Usabilidade, portabilidade, testabilidade, manutenibilidade, reprodutibilidade Ver seção final
Custo e tempo Cabe no orçamento e no prazo? É o que limita todos os outros.

Os atributos se influenciam e entram em conflito: mais redundância melhora a confiabilidade e piora o custo; mais segurança pode piorar a usabilidade; um bom desempenho com poucos usuários não garante escalabilidade. Por isso o trabalho de quem projeta é escolher trade-offs conscientes, e não buscar a solução perfeita. Cada decisão tem um lado bom e um lado ruim — diga qual é o lado ruim em voz alta.

Estilos de arquitetura: o ponto de partida

Estilo Ideia Pontos fortes Pontos fracos
Monólito em camadas Uma única unidade, dividida em apresentação, regras de negócio e dados Simples, barato, fácil de implantar e de manter o desempenho Implantação e escala "tudo ou nada"; uma falha derruba tudo
Microkernel (plugins) Núcleo mínimo + extensões (como uma IDE com plugins) Extensível, bom para aplicações instaláveis Mesmos limites de monólito
Orientada a serviços (SOA) Poucos serviços grandes por domínio, comunicação em rede Times independentes, escala por domínio Acesso a dados de outro domínio vira chamada de rede (ou acoplamento pelo banco)
Microsserviços Muitos serviços pequenos, cada um com seu banco, atrás de um API gateway Implantação e escala independentes, tolerância a falhas Latência de rede, complexidade operacional, custo alto
Orientada a eventos Serviços reagem a eventos publicados em filas/tópicos Desacopla e suporta picos Fluxos mais difíceis de acompanhar
Hexagonal Núcleo de domínio isolado, com adaptadores de entrada e saída Fácil de testar e trocar tecnologias Mais camadas de abstração

Detalhes em Padrões arquiteturais, Microsserviços e Arquitetura orientada a eventos.

Regra prática

Nenhum modelo é superior em tudo. Comece simples (um monólito bem organizado costuma bastar), meça, e divida em serviços quando a escala ou a organização dos times pedir. A arquitetura evolui com o sistema; começar com dezenas de microsserviços cedo demais é caro e prematuro.

Desempenho

Desempenho é o tempo de resposta sob uma carga conhecida (uma rota simples de e-commerce que passa de 1 segundo já merece investigação). Os culpados mais comuns: algoritmo ruim, acessos repetidos a recursos externos (rede, arquivo, banco) e falta de índices.

1. Escolher a estrutura de dados e o algoritmo certos

Trocar dois laços aninhados (complexidade O(n²)) por uma busca em dicionário/hash (O(n)) muda tudo. Em um teste do livro, somar pagamentos por cliente com 100 mil clientes e 100 mil pagamentos levou cerca de 27 ms na versão linear contra mais de 100 segundos na quadrática.

# O(n²): para cada cliente, percorre todos os pagamentos
for c in clientes:
    for p in pagamentos:
        if p.cpf == c.cpf:
            c.total += p.valor

# O(n): um dicionário acumula o total por CPF; cada cliente é uma consulta O(1)
totais = {}
for p in pagamentos:
    totais[p.cpf] = totais.get(p.cpf, 0) + p.valor
for c in clientes:
    c.total = totais.get(c.cpf, 0)

Veja Estrutura de dados para complexidade e tabelas de dispersão.

2. Cache

Acessar recurso externo é lento e arriscado (a rede cai, o banco sobrecarrega). Cache é uma estrutura — geralmente em memória — que guarda temporariamente o resultado para não repetir a chamada. Num teste do livro, buscar uma pessoa no banco levou ~15 ms contra ~1 ms vindo do cache.

Cache simples tem armadilhas, e por isso se usa ferramenta própria (Redis, Memcached, cache do framework) em vez de um mapa feito à mão:

  • Invalidação: quando o dado muda ou é excluído, o cache precisa ser atualizado ou removido, senão serve dado velho (stale).
  • Memória: limite de tamanho e política de despejo (LRU, TTL).
  • Concorrência e várias instâncias: cada instância teria seu cache; o ideal é um cache compartilhado.

Para HTTP, veja Cache HTTP.

3. Índices no banco de dados

Sem índice, uma consulta por uma coluna que não é chave faz full table scan (lê a tabela inteira); com índice, vai direto ao registro (estrutura de árvore). A chave primária já é indexada; colunas usadas com frequência em filtros e junções devem ganhar índice (CREATE INDEX ... ON tabela (coluna)). O custo: escritas mais lentas e mais espaço. Use o plano de execução (EXPLAIN) para decidir — SQL e NoSQL.

4. Comunicação assíncrona

Em vez de fazer tudo dentro da requisição (validar estoque, calcular frete, cobrar o cartão — lento e sujeito a falha do gateway de pagamento), a rota apenas registra o pedido numa fila e responde na hora ("recebemos seu pedido"); outro processo consome a fila e faz o trabalho pesado, avisando por e-mail ou notificação. A parte que recebe fica simples e aguenta picos; o processamento pode escalar à parte. Veja Arquitetura orientada a eventos.

flowchart LR
    C[Cliente] -->|1. envia pedido| API[API<br/>só enfileira]
    API -->|2. 202 Accepted| C
    API --> F[(Fila)]
    F --> W1[Worker 1] & W2[Worker 2]
    W1 & W2 --> P[Estoque, frete,<br/>pagamento]
    P -->|3. confirmação| C

Escalabilidade e elasticidade

Definição: escalabilidade e elasticidade

Escalabilidade: capacidade de continuar atendendo bem à medida que a demanda cresce. Elasticidade: capacidade de aumentar e diminuir recursos automaticamente conforme a demanda varia, pagando só pelo que se usa.

Vertical (scale up) Horizontal (scale out)
O que muda Máquina maior (mais CPU e RAM) Mais máquinas iguais atrás de um balanceador
Limite Físico e de custo (o preço sobe mais que proporcionalmente) Praticamente nenhum, se a aplicação for stateless
Disponibilidade Continua com ponto único de falha Mais máquinas = mais redundância
Complexidade Baixa Maior (sessão, cache e dados compartilhados)

Na infraestrutura:

  • Hardware: mais CPUs e memória aumentam o número de requisições simultâneas atendidas (o servidor usa um pool de threads limitado). É a solução mais simples, mas tem teto e custa caro.
  • Balanceador de carga: distribui as requisições entre várias instâncias (sorteio, rodízio, menos conexões) e só envia para as que passam nas verificações de saúde. Melhora desempenho e confiabilidade. Observação do livro: duas máquinas pequenas atrás de um balanceador atenderam melhor sob carga alta do que uma máquina maior, e se uma falha, a outra continua. Veja Nuvem para Auto Scaling.
  • CDN (Content Delivery Network): cache de conteúdo estático (imagens, vídeos, HTML, JS/CSS) em servidores espalhados pelo mundo, perto do usuário. Ganha em desempenho (menos distância), escalabilidade (o servidor de origem não recebe tudo) e tolerância a falhas (redundância). Custo: manter a rede e atualizar o conteúdo em cache.
  • Elasticidade: em um app de entrega de comida, a demanda é baixa de madrugada e sobe no almoço e no jantar; escalar automaticamente de 2 para 8 máquinas só nesses horários custa bem menos do que manter 8 o dia todo (a nuvem cobra por hora).

Para escalar horizontalmente

Mantenha a aplicação sem estado na instância (sessão em cache/token, arquivos em armazenamento compartilhado), e planeje o banco: réplicas de leitura, sharding e cache (veja Administração de banco).

Confiabilidade

Em qualquer sistema com muitos usuários haverá falhas: bugs, entrada inválida, disco cheio, rede, data center. Confiabilidade não é "nunca falhar", e sim continuar útil e se recuperar rápido.

Tolerância a falhas e tratamento de erros

  • Tratar exceções com granularidade: capturar tipos específicos (arquivo não encontrado x erro de leitura), liberar recursos no finally/try-with-resources, e traduzir o erro para o cliente.
  • Códigos HTTP corretos (2xx sucesso; 4xx erro do cliente; 5xx erro do servidor): o cliente decide o que fazer com um 404 (não adianta repetir) ou um 503 (pode tentar de novo). Uma exceção de "pessoa não encontrada" deve virar 404 com mensagem clara, e não 500. Veja Tratando erros.
  • Timeouts: todo acesso externo precisa de prazo. Esperar para sempre por um serviço lento esgota as threads do servidor e derruba o sistema todo (falha em cascata). Tipos: conexão, leitura/recebimento, transmissão e obtenção de conexão do pool. Muitos clientes HTTP têm timeout infinito por padrão — configure explicitamente.
  • Limitação de taxa (rate limiting): limita requisições por cliente (por chave de API, IP ou plano contratado) e responde 429 Too Many Requests. Protege contra abuso e contra um cliente derrubar o serviço para todos. Pode ser implementado no API gateway, em um filtro HTTP, ou por anotação; contadores com expiração em Redis são uma técnica comum.
  • Monitoramento e métricas: instrumentar o código para expor indicadores de saúde (latência, taxa de erros, uso de recursos) e coletá-los com ferramentas como Prometheus (coleta) e Grafana (painéis), com alertas. Em Spring Boot, o Actuator expõe essas métricas. Detalhes em SRE.

Disponibilidade

Disponibilidade é a fração do tempo em que o sistema está operando normalmente; é medida em "noves":

\[ ext{Disponibilidade} = rac{ ext{tempo no ar}}{ ext{tempo total}} = rac{MTBF}{MTBF + MTTR}\]
Disponibilidade Indisponibilidade por ano (aprox.)
99% 3,65 dias
99,9% 8,8 horas
99,99% 53 minutos
99,999% 5 minutos

Cada "nove" a mais custa muito mais caro. Por isso defina o nível necessário (SLO, veja SRE) e invista de acordo. Como alcançar:

  1. Redundância com balanceador: várias instâncias iguais.
  2. Várias zonas de disponibilidade (AZs), para sobreviver à queda de um data center.
  3. Várias regiões, com roteamento global, para sobreviver à queda de uma região inteira (maior custo e complexidade, com replicação de dados entre regiões).

Redundância torna o sistema resiliente, não infalível — continua preciso planejar a recuperação.

Recuperabilidade

  • Backups: o do banco é o principal, pois os dados normalmente não podem ser regerados. A frequência depende do sistema (um banco com milhares de transações por minuto exige cópias e logs contínuos; um sistema de notas que muda poucas vezes por semestre, bem menos). Atenção à retenção (custo de espaço e leis de proteção de dados). Tipos: completo (tudo, simples de restaurar, ocupa muito), incremental (só o que mudou desde o último backup de qualquer tipo, pequeno, restauração em cadeia) e diferencial (o que mudou desde o último completo).
  • Reinício automático: garantir que o serviço volte sozinho após queda ou reinicialização da máquina (Restart=always no systemd; --restart always / unless-stopped / on-failure no Docker; restart policy e orquestrador no Kubernetes).
  • Plano de contingência (runbook): documento com cenários de falha prováveis, responsáveis e canais de comunicação, procedimentos de recuperação passo a passo e a infraestrutura de backup. Evita depender de uma única pessoa que "sabe tudo" (bus factor = 1).

Implantação (deployability)

A troca de versão é um dos momentos mais arriscados. Para reduzi-lo:

flowchart LR
    DEV[Dev] --> TST[Testes / QA] --> HML[Homologação] --> PRD[Produção]
    PRD -.rollback.-> PRD
  • CI/CD automatizado (por exemplo, GitHub Actions): a cada push na branch principal o pipeline compila, testa, constrói a imagem de contêiner, publica no registry e implanta. Ambientes separados e progressivamente mais confiáveis (desenvolvimento, testes, homologação, produção).
  • Rollback fácil: voltar à versão anterior de forma automática e rápida; reverter o pull request no Git e deixar o pipeline reimplantar é um caminho simples. Estratégias como blue/green e canary reduzem o risco (CI/CD).
  • Infraestrutura como código (IaC): descrever máquinas, redes e permissões em arquivos versionados (Terraform, scripts com SDK da nuvem), para criar e refazer o ambiente de forma repetível e revisável, sem cliques manuais.
  • Segredos fora do código: nunca coloque chaves e senhas no repositório ou no pipeline; use o cofre de segredos (Vault, secrets do provedor ou do CI) e injete como variável de ambiente.

Segurança

Segurança é talvez o requisito mais abrangente e mais negligenciado (a falta dela só aparece depois do incidente). Ela precisa existir em todas as camadas: código, infraestrutura e pessoas. A base é a tríade CIA — confidencialidade, integridade e disponibilidade (Segurança).

No código

Tema Boas práticas
Autenticação Não invente: use padrões e ferramentas prontas (OAuth 2.0, OpenID Connect, SAML 2.0, Keycloak, Auth0, Spring Security). Em APIs sem sessão, o cliente envia um token (por exemplo, JWT: cabeçalho + claims + assinatura, em Base64) a cada requisição, e o servidor valida a assinatura, sem armazenar um token por usuário
Autorização Validar papel (role) por rota, de preferência por anotação ou filtro em vez de código repetido em cada rota
Senhas Guardar só o hash com sal (BCrypt, Argon2). MD5 e SHA-1 não servem para senhas
Dados sensíveis Criptografia em trânsito (TLS) e em repouso; nunca registrar senhas em logs
Auditoria Trilha de auditoria: quem, quando, qual ação e com quais dados (mascarando os sensíveis)
Dependências Monitorar vulnerabilidades conhecidas (Dependabot, catálogo CVE). Exemplo marcante: Log4Shell (CVE-2021-44228), em que uma expressão numa mensagem de log permitia execução remota de código

Principais ataques web (catálogo completo em OWASP):

Ataque Como funciona Defesa
SQL Injection Entrada do usuário concatenada em SQL altera o comando ('; DROP TABLE ...--) Consultas parametrizadas (prepared statements), validação e saneamento
XSS (cross-site scripting) Script malicioso armazenado e executado no navegador de outra pessoa Validar a entrada e escapar a saída; política de conteúdo (CSP)
CSRF Site malicioso faz o navegador do usuário autenticado enviar uma requisição ao seu sistema Tokens anti-CSRF, cookies SameSite, configuração correta de CORS (CORS)

Na infraestrutura

  • Controle de acesso e privilégio mínimo: cada pessoa ou sistema recebe só o necessário; ambientes de produção e teste em contas separadas; acesso à produção para pouquíssimas pessoas e, de preferência, por ferramentas que controlam e registram o uso; bloquear contas inativas; autenticação forte e MFA; chaves SSH em vez de senha; treinamento de quem opera a infraestrutura.
  • Segredos em cofre: chaves e senhas fora do código, sem leitura em texto claro nem por administradores.
  • Monitoramento de segurança: logs do sistema operacional e de servidores, tráfego de rede e de e-mail, vulnerabilidades conhecidas — com ferramentas automatizadas; atualizar dependências e aplicar patches (equilibrando risco e compatibilidade).
  • Auditoria: usuários individuais (não contas compartilhadas como ec2-user) e registro de acessos a servidores, código-fonte, rede, execuções de CI/CD e nuvem; exigência de certificações como ISO 27001 e SOC 2 (e PCI DSS para cartões), que cobram desde pessoas até aplicação e infraestrutura.
  • Integridade: verificar que algo não foi adulterado — por exemplo, conferir o hash de um arquivo baixado, assinar artefatos, proteger dados no banco contra alterações indevidas.
Ataque de infraestrutura Mitigação
DoS/DDoS Rede de proteção do provedor, CDN, balanceador/gateway, limite de taxa
Força bruta Bloqueio por tentativas, MFA, senhas fortes, chaves em vez de senha
Varredura de portas Deixar abertas só as portas necessárias e para origens conhecidas (firewall, grupos de segurança)
Escalada de privilégio Privilégio mínimo, usuários sem acesso administrativo desnecessário, patches
Ransomware Backups testados e isolados, privilégios baixos, redes segregadas, software atualizado, treinamento contra phishing

Mais sobre nuvem e segurança em camadas em Nuvem.

Outros requisitos

Requisito O que é Como atender
Usabilidade Facilidade e eficiência de uso Interface intuitiva, navegação clara, feedback imediato, ajuda contextual; depende de desempenho e confiabilidade
Portabilidade Facilidade de mover o sistema entre ambientes Evitar vendor lock-in (recursos exclusivos de um banco ou nuvem), usar padrões abertos e camadas de abstração; veja lock-in
Corretude O sistema faz exatamente o que foi especificado Testes funcionais; em sistemas críticos (aviação, saúde, finanças) até verificação formal
Testabilidade Facilidade de testar Modularização, isolamento de dependências (mocks), métodos determinísticos, medição de cobertura (Qualidade)
Manutenibilidade Facilidade de manter e evoluir Código limpo, análise estática (SonarQube), gestão de dívida técnica (Boas práticas)
Reprodutibilidade Mesmo resultado em ambientes e execuções diferentes Contêineres, IaC, versões fixadas; essencial para testes, auditoria e colaboração (sistemas probabilísticos, como LLMs, nem sempre a atendem)
Documentação Interna (arquitetura), para usuários e de segurança Mantê-la viva e versionada junto ao código (ADR, README)
Custo e tempo Os limites de todo projeto Processos como Scrum ajudam a medir e renegociar escopo, prazo e custo (Gestão de projetos)

Como usar isto em uma entrevista de system design

(Complemento do projeto; não está no livro-fonte.)

Um roteiro que funciona para responder "projete um sistema de X":

  1. Esclareça os requisitos: funcionais, e os não funcionais com números (usuários, requisições por segundo, latência-alvo, disponibilidade, volume de dados, regiões).
  2. Estime a escala: leituras x escritas, tamanho dos dados, picos.
  3. Desenhe o básico: cliente → balanceador → serviço(s) → banco, com um monólito ou poucos serviços.
  4. Aponte os gargalos e aplique as ferramentas: cache, índices, fila assíncrona, CDN, réplicas de leitura, sharding, escala horizontal.
  5. Trate falhas: timeouts, retry com limite, limitação de taxa, múltiplas AZs, backup e rollback.
  6. Cubra segurança: autenticação/autorização, criptografia, segredos, auditoria.
  7. Explicite os trade-offs e o que mudaria se a carga crescesse 10 vezes.

Perguntas frequentes: vertical x horizontal?, como evitar ponto único de falha?, quando usar cache e como invalidá-lo?, por que fila em vez de chamada síncrona?, como medir disponibilidade?, como implantar sem derrubar o sistema?, como armazenar senhas?.