Aprenda System Design com um sistema de agendamentos: requisitos, dados, capacidade, falhas e trade-offs para projetar sem complicar antes da hora.


Uma tela de agendamento parece simples: mostrar os horários livres, deixar o usuário escolher um e salvar a reserva.
Agora duas pessoas escolhem o mesmo horário ao mesmo tempo. As duas recebem confirmação. Em outra tentativa, a reserva é gravada, mas a resposta se perde na rede. O usuário tenta novamente. Enquanto isso, o provedor de e-mail fica lento e a tela continua carregando.
É comum associar System Design a empresas gigantes, a entrevistas de big tech ou a sistemas com milhões de usuários. Faz sentido: é nesses lugares que o tema mais aparece.
Só que esse cenário fictício não precisa de nada disso para dar errado. Bastam concorrência, uma falha de comunicação e decisões que ficaram implícitas. Uma clínica com três profissionais já reúne tudo isso: dois pacientes escolhendo o mesmo horário, uma resposta que se perde na rede e ninguém que tenha decidido o que acontece quando o e-mail atrasa.
Organizar controllers, services e repositories ajuda a construir o código. Mas ainda precisamos decidir quem confirma a reserva, quais dados precisam ser protegidos e o que continua funcionando quando uma dependência falha.
É nesse espaço que entra System Design.
Vamos acompanhar esse sistema de agendamentos do problema ao primeiro desenho, sem começar por uma lista de tecnologias.
TL;DR
System Design é projetar como componentes, dados e comunicações trabalham juntos para atender a requisitos reais.
Comece pelo comportamento esperado, pelos limites e pelo que não pode dar errado. Depois desenhe uma solução inicial, confronte-a com carga e falhas e acrescente complexidade quando houver um motivo concreto.
System Design é a atividade de definir responsabilidades, interfaces, dados e interações para que um sistema entregue o comportamento e a qualidade esperados. O desenho precisa considerar tanto as funcionalidades quanto requisitos de desempenho, segurança, disponibilidade e evolução.
Não é apenas escolher onde colocar um cache. É entender qual trabalho ele evitaria, quanto tempo o dado poderia ficar desatualizado e o que aconteceria se ele saísse do ar.
Uma distinção prática: System Design é o trabalho de conectar necessidade e solução. Arquitetura de software é o desenho estrutural e o conjunto de decisões que esse trabalho ajuda a produzir e revisar. Os termos se sobrepõem; essa distinção serve para orientar a conversa, não para criar uma fronteira rígida.
A pergunta inicial não é qual tecnologia usar. É qual compromisso o sistema precisa cumprir.
Requisitos definem o que a solução precisa entregar; restrições limitam os caminhos disponíveis. Sem esses dois elementos, comparar arquiteturas vira uma disputa de preferências. Tanto o método de projeto do quanto as orientações da Microsoft partem dessas informações para orientar decisões.
Para o nosso sistema, vamos assumir horários fixos de 30 minutos, previamente cadastrados, com uma vaga por profissional em cada horário. Usuários consultam e cancelam seus agendamentos; gestores acompanham as agendas dos profissionais das equipes sob sua responsabilidade.
Pagamentos, recorrência e integrações com calendários externos ficam fora desta primeira versão.
Cada tipo de requisito cobra algo diferente do desenho. Para o agendamento, as primeiras definições seriam estas. Os números são hipóteses didáticas, não recomendações universais:
| Natureza | Pergunta | No sistema de agendamentos |
|---|---|---|
| Funcionalidades | O que precisa acontecer? | Consultar disponibilidade, reservar, cancelar e acompanhar agendas autorizadas. |
| Qualidade | Em quais condições deve funcionar? | Responder a 95% das consultas em até 300 ms na API, sob a carga-alvo de pico. |
| Restrições | Que limites reduzem as escolhas? | Quatro desenvolvedores, oito semanas e infraestrutura já utilizada pela empresa. |
Cada linha já mexe no desenho:
Uma invariante é uma propriedade que o sistema precisa preservar durante suas operações. No nosso caso:
Um horário não pode ter duas reservas ativas.
Essa regra vale quando duas pessoas clicam juntas, quando uma requisição é repetida e quando existem várias instâncias, ou cópias em execução, da aplicação.
Outra regra do exemplo é que ser gestor não concede acesso a qualquer agenda. O acesso depende de qual equipe aquela pessoa pode acompanhar.
Antes de desenhar a solução, já encontramos duas responsabilidades: proteger a ocupação do horário e verificar o escopo de acesso. Elas não podem depender apenas da tela.
Uma estimativa de capacidade serve para orientar escolhas, não para prever o futuro com precisão. O que interessa primeiro é a ordem de grandeza, separando tráfego normal, picos e crescimento.
Suponha 10 mil usuários ativos por dia, cada um fazendo vinte consultas de disponibilidade:
Isso não quer dizer que três consultas por segundo bastem. A demanda não se espalha pelo dia: ela se concentra quando a agenda abre. Se mil dessas pessoas entrarem nos mesmos cinco minutos, a mesma conta muda de escala:
Com margem para o que a estimativa não vê, vamos estabelecer uma carga-alvo de teste de 100 consultas e 20 tentativas de reserva por segundo. As 20 tentativas cobrem um pedido de cada uma dessas mil pessoas no primeiro minuto. É uma hipótese de pico a validar, não uma medição.
O crescimento pede outra conta, a do armazenamento. Se tivermos cinco mil novas reservas por dia, com 2 KB lógicos cada, serão cerca de 10 MB por dia ou 3,65 GB por ano, usando unidades decimais. Histórico adicional, índices, réplicas e backups ainda não entraram, e o artigo sobre estimativa de storage mostra como somá-los.
Mesmo multiplicado algumas vezes, esse volume cabe com folga num único banco relacional. A conta não dimensiona o banco sozinha, mas impede que “precisamos de um banco distribuído” apareça sem justificativa.
Também importa onde a demanda se concentra. As 20 tentativas por segundo podem ser para vinte profissionais diferentes ou todas para o mesmo horário: são testes distintos, e o total de requisições não conta essa parte da história.
O primeiro desenho deve mostrar responsabilidades e caminhos de dados, não todos os detalhes da infraestrutura. O modelo organiza essas visões por níveis de aproximação; aqui, basta uma visão lógica pequena para discutir as escolhas principais.
Com quatro desenvolvedores e oito semanas, eu começaria com uma aplicação modular e um banco relacional. Agenda, Equipes e Autorização seriam módulos com responsabilidades separadas, não três microsserviços.
Um cuidaria das notificações. A reserva e a pendência de notificação seriam persistidas na mesma transação; o envio aconteceria depois.
O desenho separa o que o usuário precisa aguardar do trabalho que pode continuar em segundo plano:
Essa proposta evita introduzir comunicação de rede entre partes que o mesmo time modifica em conjunto. Microsserviços seriam uma alternativa a avaliar caso surgissem necessidades concretas de implantação, operação ou escala independentes. Não os adicionaria apenas para dividir o desenho.
O banco relacional é um candidato porque o exemplo precisa relacionar usuários, equipes e reservas e proteger alterações transacionais. Não porque “SQL é sempre melhor”. A escolha específica ainda depende do que a empresa opera e do comportamento medido.
As interfaces também precisam ser explícitas. Por exemplo:
/appointmentsNesse contrato, uma resposta de sucesso significará reserva confirmada, não apenas pedido recebido para processamento futuro:
{
"id": "apt_8f21",
"slot": "H-42",
"status": "CONFIRMED"
}Um 202 Accepted prometeria só que o pedido entrou na fila. Essa distinção orienta o que precisa acontecer antes de responder.
Escolher um banco não resolve, por si só, quem pode alterar os dados nem como preservar as regras sob concorrência. No nosso desenho, o módulo de Agenda controla reservas; Equipes mantém os vínculos organizacionais usados pela autorização. Compartilhar um banco não significa permitir que qualquer módulo reescreva qualquer estado.
Imagine Ana e Bruno consultando o horário H-42. Os dois recebem “disponível”. Ana confirma primeiro, mas Bruno ainda está olhando a resposta anterior.
Se a aplicação apenas consulta e depois grava, sem uma proteção concorrente, existe espaço para ambas as operações passarem. Estar dentro de uma transação não torna automaticamente seguro qualquer fluxo de consulta seguida de escrita; o e as restrições usados importam.
Sem essa proteção, a disputa pelo H-42 termina assim:
Para os horários fixos deste exemplo, uma solução seria manter uma ocupação vigente por horário, protegida por unicidade no banco. A transação agrupa as alterações: a reserva e sua ocupação são confirmadas juntas ou desfeitas. Restrições de unicidade existem justamente para impedir valores duplicados nas colunas protegidas.
As duas solicitações passam pelo mesmo ponto de proteção:
Quem perde a disputa recebe um conflito, não uma confirmação que será desfeita silenciosamente:
{
"code": "SLOT_UNAVAILABLE",
"message": "Este horário acabou de ser reservado. Escolha outro."
}Ao cancelar, o sistema libera a ocupação e preserva o histórico da reserva. Isso também faz parte do modelo.
Só vale para horários fixos
A unicidade por horário funciona porque cada horário é uma vaga fixa. Com intervalos livres, duas reservas podem se sobrepor sem começar no mesmo instante, e impedir inícios iguais não basta.
A confirmação precisa respeitar a ocupação atual. Já um resumo gerencial poderia, se o produto aceitar, ser atualizado com alguns segundos de atraso. São compromissos diferentes dentro do mesmo sistema.
Um cache de disponibilidade poderia acelerar consultas, mas não substituiria a verificação protegida ao reservar: a cópia pode estar desatualizada.
Permissões exigem outro cuidado. A API deve verificar o usuário, a ação e o recurso acessado. Ocultar uma agenda no frontend não protege a chamada direta ao backend. A recomenda validar autorização em cada requisição.
Perceba como a regra “gestor acompanha sua equipe” virou uma decisão sobre fonte de dados, consulta e fronteira de acesso, não apenas sobre qual botão aparece na tela.
Latência é quanto uma operação demora. Throughput é quanto trabalho o sistema conclui por unidade de tempo. Escalabilidade é sua capacidade de lidar com mais carga ao ampliar recursos, preservando os objetivos definidos. Um sistema pode responder rápido com dez usuários e degradar quando a concorrência aumenta.
Escalar verticalmente é dar mais recursos a uma instância. Escalar horizontalmente é adicionar instâncias. Um balanceador distribui requisições entre elas, mas isso não elimina um gargalo compartilhado no banco. Também precisamos evitar que uma requisição dependa de estado guardado somente na memória de uma instância específica.
Outras peças entram conforme o problema observado:
| Pressão observada | Opção a avaliar | O preço da escolha |
|---|---|---|
| Muitas leituras repetidas | Cache | Reduz acesso à origem, mas exige lidar com dados desatualizados. |
| Consultas sobrecarregam o banco principal | Réplica de leitura | Distribui leituras, mas a replicação assíncrona pode apresentar atraso. |
| Picos de trabalho que pode esperar | Fila e workers | Amortecem picos, mas acrescentam espera, reprocessamento e monitoramento. |
| Dados ou escrita atingem limites de um nó | Particionamento entre nós | Distribui carga, mas complica consultas e operações entre partições. |
Esses mecanismos resolvem problemas distintos, e cada um cobra o seu preço.
No nosso exemplo, eu verificaria consultas, índices e quantidade de dados retornados antes de adicionar outra camada. Um histórico inteiro devolvido a cada acesso pede uma resposta limitada, possivelmente paginada, não necessariamente mais servidores.
E uma fila não cria capacidade infinita. Se o trabalho chega continuamente mais rápido do que os consumidores processam, o atraso cresce.
Suponha que, no pico, as reservas confirmadas gerem 20 e-mails por segundo e que o provedor aceite 10:
Em dez minutos de pico, são 6.000 e-mails esperando. E mais workers não resolvem: o limite está no provedor, não em quem consome a fila.
Um desenho precisa explicar o que acontece quando suas partes deixam de responder. Falhas transitórias, timeouts e sobrecarga fazem parte do problema, e novas tentativas mal controladas podem agravá-lo.
No sistema de agendamentos, a consequência depende de qual parte falhou.
Se o banco estiver indisponível, não prometemos uma nova reserva. Sem registrar a ocupação com segurança, responder “confirmado” quebraria o contrato do endpoint.
Se o provedor de e-mail falhar, a reserva já confirmada permanece válida. A pendência persistida permite tentar o envio depois. O worker precisa registrar sucesso ou falha, limitar tentativas e tornar pendências antigas visíveis para a operação. Neste desenho inicial, uma tabela de tarefas pode cumprir esse papel sem um separado.
Se a resposta se perder depois da confirmação, o resultado fica incerto para o cliente. Um timeout não significa que nada aconteceu. Para permitir repetições seguras, podemos identificar a solicitação e reconhecer novas tentativas da mesma operação, sem produzir outra reserva. Essa propriedade é chamada idempotência.
Com uma chave enviada pelo cliente, a repetição deixa de ser um novo pedido:
A unicidade protege o horário contra duas reservas; a idempotência ajuda a reconhecer a repetição de um mesmo pedido. Uma não substitui a outra.
O Google SRE recomenda acompanhar latência, tráfego, erros e . Para a agenda, eu acrescentaria conflitos de reserva, idade das notificações pendentes e tempo esperando conexão com o banco.
Um aumento de latência pede investigação: processamento lento, dependência externa ou espera por um recurso ocupado? Métricas mostram o comportamento agregado; o rastreamento de uma requisição ajuda a localizar onde ela gastou tempo.
O artigo de P50, P95 e P99 aprofunda essa leitura. Aqui, basta guardar que “a média está boa” não encerra a avaliação.
Também precisamos combinar quanto tempo a operação pode ficar indisponível e quanta perda de dados é tolerável em uma recuperação. Essas metas orientam redundância, backups e testes de restauração; um serviço gerenciado não elimina a necessidade de verificar o resultado.
Uma arquitetura é uma proposta a validar, não uma prova de capacidade. Para o nosso exemplo, os testes precisam confrontar o desenho com os compromissos assumidos:
Depois entram os custos que nenhum diagrama mostra: quem recebe o alerta, restaura o banco, mantém os contratos e atualiza as dependências. A avaliação de arquitetura precisa considerar esse trabalho, além da conta de infraestrutura.
E o desenho de hoje não tem um próximo passo fixo. Cada sinal pede uma mudança diferente:
A evolução responde a limites e necessidades concretas, não a um roteiro obrigatório de tecnologias.
Eu registraria a proposta em um Design Doc e preservaria as escolhas arquiteturais relevantes em ADRs. Por exemplo: por que a reserva precisa de confirmação imediata, mas a notificação pode acontecer depois?
Essa justificativa importa quando alguém propuser colocar toda a operação em uma fila para “ganhar escala”. A mudança pode ser válida, mas altera a promessa feita ao usuário e precisa ser discutida como tal.
Antes de fechar, cinco perguntas sobre requisitos, carga, concorrência, falhas e a promessa feita ao usuário:
Todas as metas abaixo falam das consultas de disponibilidade. Qual delas permite avaliar se o desenho atende?
O sistema de agendamentos começou com uma tela, um horário e um botão. O desenho ganhou forma quando definimos o que esse botão precisava garantir.
Não foi necessário começar com microsserviços, cache distribuído e várias filas. Foi necessário explicitar responsabilidades, proteger a reserva e separar a confirmação do trabalho que poderia esperar.
Aprender System Design é desenvolver esse raciocínio: ligar cada peça a um requisito, reconhecer o custo da escolha e saber qual evidência faria o desenho mudar.
Na próxima funcionalidade do seu projeto, escolha uma operação importante e siga seu caminho. Quem pode executá-la? Que estado ela altera? O que acontece se chegar duas vezes ou se a resposta não voltar?
Antes de adicionar outra caixa ao diagrama, responda a essas perguntas. Você já estará fazendo System Design.
Os padrões, os pilares e a arquitetura por trás do desenho deste artigo.
Arquiteto e Engenheiro de Software | Java & IA