96 pontos por GN⁺ 2025-08-18 | Ainda não há comentários. | Compartilhar no WhatsApp
  • Bom design de sistemas é aquele que não parece complexo e não apresenta problemas relevantes por um longo período
  • Lidar com estado (state) é a parte mais difícil do design de sistemas, e é importante reduzir ao máximo o número de componentes que armazenam estado
  • O banco de dados é, em geral, o local onde o estado fica armazenado, então é necessário focar em design de schema e indexação, além de uma abordagem voltada para eliminar gargalos
  • Cache, processamento de eventos e trabalhos em background devem ser introduzidos com cuidado para desempenho e manutenção, e é melhor evitar o uso excessivo
  • Em vez de um design complexo, usar adequadamente componentes e metodologias simples e bem validados é a chave para construir sistemas sustentáveis e estáveis

Definição de design de sistemas e abordagem geral

  • Se o design de software é a montagem do código, design de sistemas é o processo de combinar vários serviços
  • Os principais componentes do design de sistemas são app servers, bancos de dados, caches, filas, event buses e proxies
  • Um bom design gera reações como "não há nenhum problema especial", "terminou mais fácil do que eu esperava" e "não preciso me preocupar com essa parte"
  • Em contrapartida, um design complexo e chamativo pode esconder problemas fundamentais ou indicar overengineering
  • Em vez de introduzir um sistema complexo logo no início, é melhor evoluir gradualmente a partir de uma estrutura simples, mínima e funcional

A distinção entre state e stateless

  • A parte mais difícil do design de software é justamente o gerenciamento de estado
  • Serviços que não armazenam informações e retornam o resultado imediatamente (como a renderização de PDF do GitHub) são stateless
  • Já serviços que realizam escrita no banco de dados gerenciam estado
  • É recomendável reduzir ao máximo os componentes com armazenamento de estado dentro do sistema. Isso diminui a complexidade e a probabilidade de falhas
  • Recomenda-se uma estrutura em que apenas um serviço cuide do gerenciamento de estado, enquanto os demais se concentrem em papéis stateless, como chamadas de API ou geração de eventos

Design de banco de dados e pontos de gargalo

Design de schema e índices

  • Para armazenar dados, é necessário um design de schema legível por pessoas
  • Um schema flexível demais (por exemplo, armazenar tudo em uma coluna JSON) pode pesar no código da aplicação e no desempenho
  • É preciso definir índices adequados com base nas colunas que serão consultadas com frequência. Colocar índice em tudo só gera overhead desnecessário

Como resolver gargalos

  • O acesso ao banco de dados muitas vezes se torna um gargalo pesado
  • Sempre que possível, é melhor processar dados complexos dentro do banco de dados, com JOINs por exemplo, em vez de fazer isso na aplicação, por questões de desempenho
  • Ao usar ORM, é preciso tomar cuidado para não disparar queries dentro de loops
  • Dependendo da necessidade, dividir queries para ajustar a carga do banco de dados ou a complexidade da consulta também pode ser uma boa abordagem
  • Uma estratégia eficaz é distribuir queries de leitura para read replicas, reduzindo a carga do nó principal de escrita
  • Quando há um grande volume de queries, transações e operações de escrita podem facilmente sobrecarregar o banco, então vale considerar throttling de queries

Separação entre tarefas lentas e rápidas

  • Tarefas com as quais o usuário interage precisam de resposta em poucas centenas de milissegundos
  • Para tarefas demoradas (por exemplo, conversão de PDFs grandes), é eficaz o padrão de entregar imediatamente apenas o mínimo necessário no front-end e enviar o restante para background
  • Trabalhos em background normalmente operam com uma fila (por exemplo, Redis) e um job runner juntos
  • Para tarefas agendadas para um futuro mais distante, em vez de Redis, é mais prático gerenciá-las com uma tabela separada no banco e executá-las com um scheduler

Cache

  • Cache ajuda a reduzir custos e melhorar o desempenho quando a mesma operação, ou uma operação cara, é repetida
  • Em geral, engenheiros juniores que acabaram de aprender cache querem colocar cache em tudo, enquanto engenheiros experientes tendem a ser mais cautelosos ao introduzi-lo
  • Como o cache introduz um novo estado, há riscos de sincronização, erros e dados stale
  • O ideal é tentar primeiro melhorias de desempenho como adicionar índices às queries, e só depois aplicar cache
  • Para caches grandes, também é possível usar uma abordagem de armazenamento periódico em object storage, como S3 ou Azure Blob Storage, em vez de Redis/Memcached

Processamento de eventos

  • A maioria das empresas possui um event hub (por exemplo, Kafka), e vários serviços fazem processamento distribuído com base em eventos
  • Em vez de abusar de eventos, um design simples de API no modelo request–response é mais útil para logging e resolução de problemas
  • O processamento orientado a eventos é adequado quando o remetente não precisa se preocupar com o comportamento do destinatário, ou em cenários de alto volume com tolerância a latência

Formas de entrega de dados: push e pull

  • Há duas formas de entrega de dados: Pull (solicita e recebe resposta) e Push (envio automático quando há mudança)
  • O modelo Pull é simples, mas pode causar requisições repetidas e sobrecarga
  • No modelo Push, quando os dados mudam no servidor, eles são enviados imediatamente ao cliente, o que é eficiente e favorável para manter os dados atualizados
  • Para atender um grande número de clientes, é necessário expandir a infraestrutura conforme cada modelo (filas de eventos, vários servidores de cache etc.)

Foco em hot paths

  • Hot paths são os caminhos mais importantes dentro do sistema e por onde passa o maior volume de dados
  • Como hot paths oferecem poucas alternativas e uma falha de design pode causar problemas graves em todo o serviço, é essencial projetá-los com cuidado
  • Em vez de gastar recursos com funcionalidades secundárias cheias de opções, é mais eficaz concentrar design e testes nos hot paths

Logging, métricas e tracing

  • Para diagnosticar a causa de falhas, é importante registrar ativamente logs detalhados dos caminhos anômalos (unhappy path)
  • É necessário coletar métricas básicas de observabilidade, como recursos do sistema (CPU/memória), tamanho de filas e tempo de requisições/trabalhos
  • Em vez de olhar apenas médias, é indispensável observar métricas de distribuição como latência p95 e p99. Uma pequena parcela das requisições mais lentas pode representar o problema dos usuários mais importantes

Kill switch, retry e recuperação de falhas

  • O uso estratégico de kill switches (bloqueio temporário do sistema) e retries é importante
  • Tentar novamente sem critério só sobrecarrega outros serviços; para funcionar bem, é preciso controlar as requisições de antemão com recursos como circuit breakers
  • Com a adoção de Idempotency Key, é possível evitar trabalho duplicado ao reprocessar a mesma requisição
  • Em alguns cenários de falha, é preciso escolher entre fail open ou fail closed. Por exemplo, em rate limiting, fail open (permitir) tende a causar menos impacto ao usuário. Já em autenticação, fail closed é indispensável

Encerramento

  • Alguns temas, como separação de serviços, containers, adoção de VM e tracing, foram omitidos, mas usar componentes bem validados nos lugares certos continua sendo a forma mais estável de construir sistemas no longo prazo
  • Designs tecnicamente especiais são, na prática, muito raros; um design simples a ponto de parecer entediante é justamente o mais usado no trabalho real
  • Em essência, um bom design de sistemas é o processo de combinar com segurança metodologias comprovadas de forma discreta e sem chamar atenção

Ainda não há comentários.

Ainda não há comentários.