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 (
2xxsucesso;4xxerro do cliente;5xxerro 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":
| 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:
- Redundância com balanceador: várias instâncias iguais.
- Várias zonas de disponibilidade (AZs), para sobreviver à queda de um data center.
- 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=alwaysno systemd;--restart always/unless-stopped/on-failureno 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":
- 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).
- Estime a escala: leituras x escritas, tamanho dos dados, picos.
- Desenhe o básico: cliente → balanceador → serviço(s) → banco, com um monólito ou poucos serviços.
- Aponte os gargalos e aplique as ferramentas: cache, índices, fila assíncrona, CDN, réplicas de leitura, sharding, escala horizontal.
- Trate falhas: timeouts, retry com limite, limitação de taxa, múltiplas AZs, backup e rollback.
- Cubra segurança: autenticação/autorização, criptografia, segredos, auditoria.
- 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?.