Engenharia em 2026 e além
Estamos construindo uma infraestrutura que quase nunca pode falhar. Para conseguir isso, avançamos rápido enquanto lançamos software de qualidade incrível, sem atalhos nem concessões. Este documento descreve os padrões de engenharia que vão nos guiar por 2026 e além.

On this page
- Estrutura do time
- Nossos resultados até agora
- A base da Cal.com permite excelência para a coss.com
- Princípios centrais
- 1. Sem qualidade adiada
- 2. Alto padrão na revisão de código
- 3. Se puxem para fazer o certo
- 4. Busque simplicidade
- 5. Automatize tudo
- Padrões arquiteturais
- Vertical Slice Architecture: packages/features
- Por que fatias verticais importam
- Diretrizes para Organização de Funcionalidades
- Benefícios
- Padrão Repository e Injeção de Dependência
- Data Transfer Objects (DTOs)
- Padrões de Domain-Driven Design
- Consistência da base de código
- Mova Condicionais para o Ponto de Entrada da Aplicação
- Por que isso importa
- Como conseguir isso
- Design de API: Controllers finos e abstração de HTTP
- Versionamento de API e mudanças que quebram compatibilidade
- Performance e complexidade de algoritmos
- Pense em escala desde o primeiro dia
- Escolha as estruturas de dados e algoritmos certos
- Verificações automáticas de performance
- Performance é um recurso
- A realidade NP-difícil do agendamento
- Requisitos de cobertura de código
- Medindo o sucesso
Estrutura do time
Nossa organização de engenharia é composta por cinco times centrais, cada um com responsabilidades distintas:
-
Foundation Team: Foca em estabelecer e manter padrões de código e padrões arquiteturais. Este time trabalha em colaboração com outros times para definir boas práticas em toda a organização.
-
Consumer, Enterprise e Platform Teams: Times focados no produto que lançam funcionalidades rapidamente enquanto mantêm os padrões de qualidade definidos neste documento. Esses times provam que velocidade e qualidade não são mutuamente exclusivas.
-
Community Team: Responsável por revisar rapidamente os PRs da comunidade open source, fornecer feedback e conduzir esse trabalho até o merge. Este time garante que quem contribui para o nosso open source tenha uma ótima experiência e que suas contribuições atendam aos nossos padrões de qualidade. Manter uma forte cultura open-source continua moldando como a Cal.com aborda a colaboração em engenharia e o desenvolvimento de produto.
Nossos resultados até agora
Os dados falam por si. No último ano e meio, transformamos fundamentalmente como construímos software:

Praticamente dobramos nossa entrega de engenharia enquanto, ao mesmo tempo, melhoramos a qualidade. Ainda mais impressionante é a mudança no que estamos construindo:

Conseguimos realocar aproximadamente 20% do esforço de engenharia das correções para funcionalidades, melhorias de performance, refatorações e tarefas operacionais. Essa mudança mostra que investir em qualidade e arquitetura não te faz andar mais devagar. Te acelera.
A base da Cal.com permite excelência para a coss.com
-
Cal.com é um negócio estável e lucrativo que vamos continuar fazendo crescer.
- Esse sucesso nos dá uma vantagem única enquanto construímos a coss.com. Diferente dos primeiros dias da Cal.com, quando precisávamos avançar rápido para encontrar product-market fit e construir um negócio sustentável, a coss.com começa a partir de uma posição de força.
-
Não precisamos correr com a coss.com.
- A estabilidade da Cal.com significa que podemos construir a coss.com do jeito certo desde o primeiro dia. Temos o privilégio de implementar esses padrões de engenharia sem a pressão de demandas imediatas do mercado ou restrições de financiamento. Este é um ponto de partida fundamentalmente diferente.
-
A "lentidão" é um investimento, não um custo.
- Sim, seguir esses padrões pode parecer mais lento no começo e até frustrar alguns engenheiros. Escrever DTOs leva mais tempo do que passar tipos do banco diretamente para o frontend. Criar abstrações adequadas e injeção de dependência exige mais design inicial. Manter cobertura de testes de 80%+ para código novo exige disciplina. Mas essa lentidão aparente é temporária, e o retorno é exponencial.
Considere os retornos compostos...
-
Código arquitetado corretamente desde o início não precisa de grandes refatorações depois
-
Alta cobertura de testes evita bugs que, de outra forma, consumiriam semanas de depuração e hotfixes (veja 2023 até meados de 2024)
-
Abstrações adequadas tornam a adição de novas funcionalidades dramaticamente mais rápida ao longo do tempo
-
Limites claros e DTOs evitam a erosão arquitetural que acaba exigindo reescritas completas
A Cal.com trajetória mostra o que acontece quando você otimiza para velocidade imediata. Alta velocidade inicial que vai se degradando à medida que a dívida técnica se acumula, atalhos arquiteturais criam gargalos e cada vez mais tempo é gasto corrigindo problemas em vez de construir funcionalidades (veja o gráfico anterior, onde estávamos gastando 55-60% do esforço de engenharia em correções).
A trajetória da coss.com vai abraçar o poder de construir certo desde o primeiro dia. Velocidade inicial um pouco mais lenta enquanto estabelecemos padrões adequados, seguida por uma aceleração exponencial à medida que esses padrões trazem retorno e permitem um desenvolvimento mais rápido com mais confiança.
Princípios centrais
1. Sem qualidade adiada
-
Vamos minimizar o "vou fazer isso em um PR de follow-up" para refatorações pequenas.
- PRs de follow-up para melhorias pequenas quase nunca acontecem. Em vez disso, elas se acumulam como dívida técnica que nos pesa meses ou anos depois. Se uma pequena refatoração pode ser feita agora, faça agora. Follow-ups devem ser reservados para mudanças substanciais que realmente mereçam PRs separados ou para casos excepcionais e urgentes.
2. Alto padrão na revisão de código
-
Não deixe PRs passarem com vários nits só para evitar ser "a pessoa chata."
- É exatamente assim que codebases vão ficando bagunçadas com o tempo. Revisão de código não é sobre ser legal. É sobre manter os padrões de qualidade que nossa infraestrutura exige. Cada detalhe importa. Cada violação de padrão importa. Resolva tudo antes do merge, não depois.
3. Se puxem para fazer o certo
-
Nós cobramos uns dos outros qualidade
- Cortar caminho pode parecer mais rápido no momento, mas cria problemas que atrasam todo mundo depois. Quando você ver um colega prestes a fazer merge de um PR com problemas óbvios, se manifeste. Quando alguém sugerir um hack rápido em vez da solução correta, questione. Quando você sentir vontade de pular testes ou ignorar padrões arquiteturais, espere que seus colegas te contestem.
-
Isso não é sobre ser difícil ou atrasar as pessoas
- É sobre propriedade coletiva da nossa codebase e da nossa reputação. Cada atalho que uma pessoa toma vira problema de todo mundo. Cada canto cortado hoje significa mais sessões de debug, mais hotfixes e mais clientes frustrados amanhã.
-
Torne normal questionar decisões ruins, com respeito
- Se alguém disser "vamos só hardcodar isso por enquanto", a resposta esperada deve ser "o que seria preciso para fazer do jeito certo na primeira vez?" Se alguém quiser commitar código sem testes, o time deve contestar. Se alguém sugerir copiar e colar em vez de criar uma abstração adequada, aponte isso com respeito.
-
Estamos construindo algo que quase nunca pode falhar
- Esse nível de confiabilidade não acontece por acaso. Ele acontece quando cada engenheiro se sente responsável pela qualidade, não só pelo próprio código, mas pelo sistema inteiro. A gente vence como time ou falha como time.
4. Busque simplicidade
-
Priorize clareza em vez de esperteza
- O objetivo é código fácil de ler e entender rapidamente, não complexidade elegante. Sistemas simples reduzem a carga cognitiva para cada engenheiro.
-
Faça as perguntas certas a si mesmo
-
Estou realmente resolvendo o problema em questão?
-
Estou pensando demais em possíveis casos de uso futuros?
-
Considerei pelo menos 1 outra alternativa para resolver isso? Como ela se compara?
-
-
Simples não significa sem funcionalidades
- Só porque nosso objetivo é criar sistemas simples, isso não significa que eles devam parecer anêmicos e sem funcionalidades óbvias.
5. Automatize tudo
-
Aproveite a IA
-
Gere 80% do código boilerplate e não crítico usando IA, permitindo que a gente se concentre exclusivamente na lógica de negócios complexa e nas arquiteturas críticas. Teams que adotam fluxos de trabalho modernos assistidos por IA estão cada vez mais recorrendo a ferramentas como os melhores servidores MCP para melhorar a produtividade e a orquestração dos desenvolvedores.
-
Construa alertas sem ruído e tratamento inteligente de erros.
-
Testes manuais são cada vez mais coisa do passado. A IA pode construir rapidamente e de forma inteligente mega suítes de testes para a gente.
-
-
Nossa CI é o chefão final
-
Tudo neste documento de padrões é verificado antes de o código ser mesclado nos PRs
-
Nenhuma surpresa entra no main
-
As verificações são rápidas e úteis
-
Padrões arquiteturais
Estamos fazendo a transição para um modelo arquitetural rígido baseado em Vertical Slice Architecture e Domain-Driven Design (DDD). Os padrões e princípios a seguir serão aplicados com rigor nas revisões de PR e por meio de linting.
Vertical Slice Architecture: packages/features
Nossa codebase é organizada por domínio, não por camada técnica. O diretório packages/features é o coração dessa abordagem arquitetural. Cada pasta dentro dele representa uma fatia vertical completa da aplicação, guiada pelo domínio com o qual ela interage.
Estrutura:
packages/features/
availability/
bookings/
ooo/
payments/
routing/
scheduling/
...
Cada pasta de feature é uma fatia vertical independente que inclui tudo o que aquele domínio precisa:
-
Lógica de domínio: As regras de negócio centrais e as entidades específicas dessa feature
-
Serviços de aplicação: Orquestração de casos de uso para esse domínio
-
Repositórios: Acesso a dados específico das necessidades dessa feature
-
DTOs: Objetos de transferência de dados para atravessar limites
-
Componentes de UI: Componentes de frontend relacionados a essa feature (quando aplicável)
-
Testes: Testes unitários, de integração e e2e para essa feature
Por que fatias verticais importam
A arquitetura tradicional em camadas organiza por preocupações técnicas:
// Bad: Organized by layer
src/
controllers/
AvailabilityController.ts
BookingController.ts
OOOController.ts
services/
AvailabilityService.ts
BookingService.ts
OOOService.ts
repositories/
AvailabilityRepository.ts
BookingRepository.ts
OOORepository.ts
Isso cria vários problemas:
-
Mudanças em uma feature exigem tocar arquivos espalhados por vários diretórios
-
É difícil entender o que uma funcionalidade faz porque o código dela está fragmentado
-
As Teams acabam se atrapalhando quando trabalham em funcionalidades diferentes
-
Você não consegue extrair ou descontinuar uma funcionalidade com facilidade
A arquitetura de fatia vertical organiza por domínio:
// Good: Organized by domain
packages/features/
availability/
services/
AvailabilityApplicationService.ts
AvailabilityDomainService.ts
repositories/
AvailabilityRepository.ts
dto/
AvailabilityDto.ts
bookings/
services/
BookingApplicationService.ts
BookingDomainService.ts
repositories/
BookingRepository.ts
dto/
BookingDto.ts
Isso resolve estes problemas:
-
Tudo relacionado à disponibilidade fica em
packages/features/availability -
Você entende toda a funcionalidade de disponibilidade explorando um único diretório
-
As Teams podem trabalhar em funcionalidades diferentes sem conflitos (se a equipe de engenharia da Cal.com crescer, mas com certeza em coss.com nós vamos fazer com que Teams assumam pacotes importantes)
-
As funcionalidades são fracamente acopladas e podem evoluir de forma independente
Diretrizes para Organização de Funcionalidades
Na teoria, cada funcionalidade é implantável de forma independente. Mesmo que a gente não faça o deploy delas separadamente de fato, organizar desse jeito nos obriga a manter as dependências claras e o acoplamento mínimo. Esse é o princípio e o sucesso dos microsserviços, embora a gente ainda não vá implantar microsserviços.
As funcionalidades se comunicam por meio de interfaces bem definidas. Se bookings precisa de dados de availability, ele importa de @calcom/features/availability por meio de interfaces exportadas, e não mexendo em detalhes internos da implementação.
O código compartilhado fica nos lugares apropriados:
-
Utilitários agnósticos de domínio e preocupações transversais (auth, logging):
packages/lib -
Primitivas de UI compartilhadas:
packages/ui(e em breve coss.com ui)
Os limites de domínio são aplicados automaticamente. Vamos criar linting que impede mexer nos internos de funcionalidades onde você não deveria ter acesso. Se packages/features/bookings tentar importar de packages/features/availability/services/internal, o linter vai bloquear. Toda dependência entre funcionalidades deve passar pela API pública da funcionalidade.


Novas funcionalidades começam como fatias verticais. Ao construir algo novo, crie uma nova pasta em packages/features com a fatia vertical completa. Isso deixa claro o que você está construindo e mantém tudo organizado desde o primeiro dia.
Benefícios
-
Descobribilidade
- Procurando a lógica de booking? Ela está toda em
packages/features/bookings. Não precisa caçar controllers, services, repositories e utilities espalhados pelo código.
- Procurando a lógica de booking? Ela está toda em
-
Testes mais fáceis
- Teste a funcionalidade inteira como uma unidade. Você tem todas as peças em um só lugar, o que torna os testes de integração naturais e diretos.
-
Dependências mais claras
- Quando você vê
import { getAvailability } from '@calcom/features/availability', sabe exatamente de qual funcionalidade você depende. Quando as dependências ficam complexas demais, isso fica óbvio e pode ser resolvido.
- Quando você vê
Padrão Repository e Injeção de Dependência
As escolhas de tecnologia não podem vazar pela aplicação. O problema com Prisma ilustra isso perfeitamente. Hoje temos referências a Prisma espalhadas por centenas de arquivos. Isso cria um acoplamento enorme e torna mudanças de tecnologia proibitivamente caras. Estamos sentindo essa dor agora ao atualizar Prisma para v6.16. Algo que deveria ter sido apenas um refactor localizado atrás de repositories protegidos virou uma busca tortuosa e quase interminável por issues em vários apps.
O padrão daqui para frente:
-
Todo acesso ao banco de dados deve passar por classes Repository. Já temos uma boa vantagem inicial nisso.
-
Repositories são o único código que conhece Prisma (ou qualquer outro ORM). Nenhuma lógica deve ficar neles.
-
Repositories são injetados por containers de Dependency Injection
-
Se algum dia trocarmos Prisma por Drizzle ou outro ORM, as únicas mudanças necessárias são:
-
Implementações dos Repository
-
Configuração do container de DI para os novos repositories
-
Nada mais no código deve se importar ou mudar
-
Isso não é teórico. É assim que construímos sistemas mantíveis.
Data Transfer Objects (DTOs)
Tipos do banco de dados não devem vazar para o frontend. Isso virou um atalho popular no nosso stack, mas é um code smell que cria vários problemas.
-
Acoplamento tecnológico (tipos do Prisma acabam em componentes React)
-
Riscos de segurança (vazamento acidental de campos sensíveis)
-
Contratos frágeis entre servidor e cliente (isso é particularmente problemático à medida que construímos muito mais APIs)
-
Incapacidade de evoluir o schema do banco de dados de forma independente
-
Todas as conversões de DTOs passam por Zod, até mesmo para uma resposta de API, para garantir que todos os dados sejam validados antes de enviar ao usuário. Melhor falhar do que retornar algo errado.
O padrão daqui para frente:
Crie DTOs explícitos em cada fronteira arquitetural.
-
Camada de dados → Camada de aplicação → API: Transforme modelos do banco de dados em DTOs da camada de aplicação, depois transforme os DTOs da aplicação em DTOs específicos da API
-
API → Camada de aplicação → Camada de dados: Transforme DTOs da API pela camada de aplicação e em DTOs específicos dos dados
Sim, isso exige mais código. Sim, vale a pena. Fronteiras explícitas evitam a erosão arquitetural que cria pesadelos de manutenção no longo prazo.
Padrões de Domain-Driven Design
Os padrões a seguir precisam ser usados corretamente e de forma consistente:
-
Application Services
- Orquestram casos de uso, coordenam entre domain services e repositories
-
Domain Services
- Contêm lógica de negócio que não pertence naturalmente a uma única entidade
-
Repositories
- Abstraem o acesso a dados, isolam escolhas de tecnologia
-
Dependency Injection
- Permitem baixo acoplamento, facilitam testes, isolam preocupações
-
Caching Proxies
-
Encapsulam repositories ou services para adicionar comportamento de cache de forma transparente
-
Não é a única forma de fazer cache, claro, mas é um bom ponto de partida
-
-
Decorators
- Adicionam preocupações transversais (logging, métricas etc.) sem poluir a lógica de domínio
Consistência da base de código
Nossas bases de código deveriam parecer que uma pessoa escreveu tudo. Esse nível de consistência exige adesão rigorosa aos padrões estabelecidos, regras abrangentes de linting que impõem padrões arquiteturais, code reviews que rejeitam violações de padrão + a ajuda de revisores de código com IA.
Mova Condicionais para o Ponto de Entrada da Aplicação
If statements pertencem ao ponto de entrada, não espalhadas pelos seus services. Esse é um dos princípios arquiteturais mais importantes para manter um código limpo e focado, que não descamba para uma complexidade impossível de manter.
Veja como o código se degrada ao longo do tempo: um service é escrito para um propósito claro e específico. A lógica é limpa e focada. Aí chega um novo requisito de produto, e alguém adiciona um if statement. Alguns anos e vários outros requisitos depois, esse service está cheio de verificações condicionais para cenários diferentes. O service se tornou:
-
Complicado e difícil de ler
-
Difícil de entender e raciocinar
-
Mais suscetível a bugs
-
Violando responsabilidade única (lidando com casos demais)
-
Quase impossível de testar completamente
O service ultrapassou seus limites em termos de responsabilidades e lógica.
Uma solução: padrão Factory com services especializados
Use o padrão factory para tomar decisões no ponto de entrada e depois delegue para services especializados que tratam sua lógica específica sem condicionais.
Exemplo da nossa base de código:
A BillingPortalServiceFactory determina se o billing é para uma organização, um time ou um usuário individual, e então retorna o service apropriado:
export class BillingPortalServiceFactory {
static async createService(teamId: number): Promise<BillingPortalService> {
const team = await teamRepository.findById({ id: teamId });
if (!team) {
throw new Error("Team not found");
}
// Decision made HERE at the entry point
if (team.isOrganization) {
return new OrganizationBillingPortalService();
}
return new TeamBillingPortalService();
}
static createUserService(): UserBillingPortalService {
return new UserBillingPortalService();
}
}
Cada service então cuida da sua lógica específica sem precisar perguntar "eu sou uma organização ou um time?":
// OrganizationBillingPortalService handles ONLY organization logic
class OrganizationBillingPortalService extends BillingPortalService {
async checkPermissions(userId: number, teamId: number): Promise<boolean> {
return await this.permissionService.checkPermission({
userId,
teamId,
permission: "organization.manageBilling", // Organization-specific
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
});
}
// ... more organization-specific logic
}
// TeamBillingPortalService handles ONLY team logic
class TeamBillingPortalService extends BillingPortalService {
async checkPermissions(userId: number, teamId: number): Promise<boolean> {
return await this.permissionService.checkPermission({
userId,
teamId,
permission: "team.manageBilling", // Team-specific
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
});
}
// ... more team-specific logic
}
Por que isso importa
-
Os services continuam focados
- Cada service tem uma responsabilidade e não precisa saber sobre outros contextos. O
OrganizationBillingPortalServicenão contém if statements verificandoif (isTeam)ouif (isUser). Ele só sabe como lidar com organizações.
- Cada service tem uma responsabilidade e não precisa saber sobre outros contextos. O
-
As mudanças ficam isoladas
- Quando você precisa modificar a lógica de cobrança da organização, você só toca em
OrganizationBillingPortalService. Você não corre o risco de quebrar a cobrança de equipes ou usuários. Você não precisa rastrear condicionais aninhados para descobrir qual caminho seu código segue.
- Quando você precisa modificar a lógica de cobrança da organização, você só toca em
-
Testar é simples
- Teste cada serviço de forma independente com seus cenários específicos. Não há necessidade de testar toda combinação de condicionais em contextos diferentes.
-
Novos requisitos não contaminam o código existente
- por exemplo, quando você precisa adicionar cobrança enterprise com regras diferentes, você cria
EnterpriseBillingPortalService. A factory ganha mais um condicional, mas os serviços existentes continuam intocados e focados.
- por exemplo, quando você precisa adicionar cobrança enterprise com regras diferentes, você cria
Como conseguir isso
-
Empurre os condicionais para controllers, factories ou lógica de roteamento. Deixe que esses pontos de entrada decidam qual serviço usar.
-
Mantenha os services puros e focados em uma única responsabilidade. Se um service precisa verificar "que tipo eu sou?", você provavelmente precisa de vários services.
-
Prefira polimorfismo a condicionais
- Interfaces definem o contrato. Implementações concretas fornecem os detalhes.
-
Fique atento ao acúmulo de ifs
- Durante a revisão de código, se você vir um service ganhando condicionais para cenários diferentes, isso é um sinal para refatorar em services especializados.
Design de API: Controllers finos e abstração de HTTP
-
Controllers são camadas finas que cuidam apenas de preocupações de HTTP.
- Eles recebem requests, processam-nas e mapeiam os dados para DTOs que são passados para a lógica central da aplicação. Daqui para frente, nenhuma lógica de aplicação ou central deve aparecer em rotas de API ou handlers de tRPC.
-
Precisamos desacoplar a tecnologia HTTP da nossa aplicação.
- A forma como transferimos dados entre cliente e servidor (seja REST, tRPC etc.) não deve influenciar como nossa aplicação central funciona. HTTP é um mecanismo de entrega, não um motor arquitetural.
Responsabilidades dos controllers (e SOMENTE estas):
-
Receber e validar requests de entrada
-
Extrair dados de parâmetros, corpo e cabeçalhos da request
-
Transformar os dados da request em DTOs
-
Chamar os serviços de aplicação apropriados com esses DTOs
-
Transformar as respostas dos serviços de aplicação em DTOs de resposta
-
Retornar responses HTTP com os códigos de status corretos
Controllers NÃO devem:
-
Conter lógica de negócio ou regras de domínio
-
Acessar diretamente bancos de dados ou serviços externos
-
Fazer transformações complexas de dados ou cálculos
-
Tomar decisões sobre o que a aplicação deve fazer
-
Conhecer detalhes de implementação do domínio
Exemplo de padrão de controller fino:
// Good: Thin controller
export async function createBookingHandler(req: Request, res: Response) {
// Extract and validate request data
const createBookingRequest = validateCreateBookingRequest(req.body);
// Transform to DTO
const createBookingDto = {
userId: req.user.id,
eventTypeId: createBookingRequest.eventTypeId,
startTime: createBookingRequest.startTime,
// ... other fields
};
// Delegate to application service
const result = await bookingApplicationService.createBooking(createBookingDto);
// Transform response and return
const responseDto = mapBookingToResponseDto(result);
res.status(201).json(responseDto);
}
// Bad: Fat controller with business logic
export async function createBookingHandler(req: Request, res: Response) {
// This is wrong - business logic in controller
if (req.body.startTime < new Date()) {
return res.status(400).json({ error: "Cannot book in the past" });
}
// This is wrong - direct database access
const existingBooking = await prisma.booking.findFirst({
where: { eventTypeId: req.body.eventTypeId, startTime: req.body.startTime }
});
// This is wrong - complex business calculations
const duration = calculateBookingDuration(req.body.eventTypeId);
// ... more business logic
}
Versionamento de API e mudanças que quebram compatibilidade
Sem mudanças que quebrem compatibilidade. Isso é crítico. Depois que um endpoint da API é público, ele precisa continuar estável. Mudanças que quebram compatibilidade destroem a confiança dos desenvolvedores e criam pesadelos de integração para nossos usuários.
Estratégias para evitar mudanças que quebram compatibilidade:
-
Sempre adicione novos campos como opcionais
-
Use versionamento de API quando precisar mudar o comportamento existente
-
Descontinue endpoints antigos com cuidado, com caminhos de migração claros
-
Mantenha compatibilidade retroativa por pelo menos duas versões principais
Quando você precisar fazer mudanças que quebram compatibilidade:
-
Crie uma nova versão da API usando o versionamento específico por data em API v2 (talvez valha olharmos também para o versionamento nomeado que a Stripe introduziu recentemente)
-
Execute as duas versões simultaneamente durante a transição (nós já fazemos isso em API v2)
-
Forneça ferramentas automatizadas de migração quando possível
-
Dê aos usuários bastante tempo para migrar (mínimo de 6 meses para APIs públicas)
-
Documente exatamente o que mudou e por quê
Performance e complexidade de algoritmos
Nós construímos para grandes organizações e equipes. O que funciona bem com 10 usuários ou 50 registros pode desabar sob o peso de escala enterprise. Performance não é algo que otimizamos depois. É algo que construímos direito desde o início.
Pense em escala desde o primeiro dia
Ao construir funcionalidades, sempre pergunte: "Como isso se comporta com 1.000 usuários? 10.000 registros? 100.000 operações?" A diferença entre algoritmos O(n) e O(n²) pode ser imperceptível no desenvolvimento, mas catastrófica em produção.
Padrões comuns O(n²) para evitar:
-
Iterações aninhadas em arrays (
.mapdentro de.map,.forEachdentro de.forEach) -
Métodos de array como
.some,.findou.filterdentro de loops ou callbacks -
Verificar cada item contra todos os outros sem otimização
-
Filtros encadeados ou mapeamento aninhado em listas grandes
Exemplo real: Para 100 slots disponíveis e 50 períodos ocupados, um algoritmo O(n²) executa 5.000 verificações. Escale isso para 500 slots e 200 períodos ocupados, e você faz 100.000 operações. Isso é um aumento de 20x na carga computacional para apenas um aumento de 5x nos dados.
Escolha as estruturas de dados e algoritmos certos
A maioria dos problemas de performance é resolvida escolhendo melhores estruturas de dados e algoritmos:
-
Ordenação + saída antecipada: ordene seus dados uma vez e depois saia dos loops quando souber que os itens restantes não vão corresponder
-
Busca binária: use busca binária para consultas em arrays ordenados em vez de varreduras lineares
-
Técnicas de dois ponteiros: para mesclar ou intersectar sequências ordenadas, percorra ambas com ponteiros em vez de loops aninhados
-
Hash maps/sets: use objetos ou Sets para buscas O(1) em vez de
.findou.includesem arrays -
Árvores de intervalos: para agendamento, disponibilidade e consultas de intervalo, use estruturas de árvore adequadas em vez de comparação na força bruta
Transformação de exemplo:
// Bad: O(n²) - checks every slot against every busy time
availableSlots.filter(slot => {
return !busyTimes.some(busy => checkOverlap(slot, busy));
});
// Good: O(n log n) - sort once, break early
const sortedBusy = [...busyTimes].sort((a, b) => a.start - b.start);
availableSlots.filter(slot => {
for (const busy of sortedBusy) {
if (busy.start >= slot.end) break; // No more overlaps possible
if (busy.end <= slot.start) continue; // This one doesn't overlap
return false; // Overlap found
}
return true;
});
Verificações automáticas de performance
Vamos implementar várias camadas de defesa contra regressões de performance:
Regras de lint que sinalizam:
-
Funções com loops aninhados ou métodos de array aninhados
-
Múltiplas chamadas aninhadas de
.some,.findou.filter -
Recursão sem memoização
-
Anti-patterns conhecidos do nosso domínio (agendamento, verificações de disponibilidade etc.)
Benchmarks de performance no CI que:
-
Executam algoritmos críticos com dados realistas e em grande escala
-
Comparam os tempos de execução com a linha de base em cada PR
-
Bloqueiam merges que introduzam regressões de performance
-
Testam com dados em escala enterprise (milhares de usuários, dezenas de milhares de registros)
Monitoramento em produção que:
-
Acompanha o tempo de execução dos caminhos críticos
-
Alerta quando os algoritmos ficam mais lentos conforme os dados crescem
-
Detecta regressões antes que os usuários percebam
-
Fornece dados reais de performance para orientar otimizações
Performance é um recurso
Performance não é opcional. Não é algo que a gente "deixa para depois." Para clientes enterprise que fazem agendamentos em equipes grandes, respostas lentas significam perda de produtividade e usuários frustrados (nossa experiência com alguns clientes enterprise maiores pode comprovar isso).
Todo engenheiro deveria:
-
Fazer o perfil do código antes de otimizar, mas pensar na complexidade desde o início
-
Testar com dados realistas e em grande escala (não apenas 5 registros de teste). Já temos scripts de seed prontos. Provavelmente precisamos estender.
-
Escolher algoritmos e estruturas de dados eficientes desde o começo
-
Ficar atento a iterações aninhadas na revisão de código
-
Questionar qualquer algoritmo que escale com o produto de duas variáveis
A realidade NP-difícil do agendamento
Problemas de agendamento são fundamentalmente NP-difíceis. Isso significa que, à medida que o número de restrições, participantes ou horários disponíveis cresce, a complexidade computacional pode explodir exponencialmente. A maioria dos algoritmos de agendamento ideais tem complexidade de tempo exponencial no pior caso, o que torna a escolha do algoritmo absolutamente crítica.
Implicações no mundo real:
-
Encontrar o horário ideal de reunião para 10 pessoas em 3 fusos horários com restrições individuais de disponibilidade é computacionalmente caro
-
Adicionar detecção de conflitos, buffers e uma infinidade de outras opções amplifica o problema
-
Más escolhas de algoritmo que funcionam bem para equipes pequenas se tornam completamente inutilizáveis para grandes organizações
-
O que leva milissegundos para 5 usuários pode levar muitos segundos para organizações
Estratégias para lidar com a complexidade NP-difícil:
-
Usar algoritmos de aproximação que encontrem soluções "boas o bastante" rapidamente, em vez de soluções perfeitas lentamente
-
Implementar cache agressivo dos agendamentos e da disponibilidade calculados
-
Pré-calcular cenários comuns fora dos horários de pico
-
Dividir grandes problemas de agendamento em partes menores e mais manejáveis
-
Definir limites razoáveis de timeout e fazer fallback para algoritmos mais simples quando necessário
É por isso que performance não é só um bônus em software de agendamento. Ela é a base que determina se o seu sistema consegue escalar para necessidades enterprise ou desaba sob padrões de uso do mundo real.
Requisitos de cobertura de código
-
Rastreamento global da cobertura
- Acompanhamos a cobertura geral da base de código como uma métrica importante que melhora com o tempo. Isso nos dá visibilidade sobre a maturidade dos nossos testes e ajuda a identificar áreas que precisam de atenção. A porcentagem global de cobertura é exibida com destaque nos nossos dashboards.
-
Cobertura de 80%+ para código novo
- Toda PR deve ter cobertura de testes perto de 80%+ para o código que ela introduz ou modifica. Isso é aplicado automaticamente no nosso pipeline de CI. Se você adicionar 50 linhas de código novo, essas 50 linhas precisam estar cobertas por testes. Se você modificar uma função existente, suas alterações precisam ser testadas. Isso é a cobertura total de testes. A cobertura de testes unitários precisa ficar perto de 100%, especialmente com a possibilidade de aproveitar IA para ajudar a gerar esses testes.
Enfrentando o argumento de que "cobertura não é a história completa": Sim, sabemos que cobertura não garante testes perfeitos. Sabemos que você pode escrever testes sem sentido que atingem todas as linhas, mas não testam nada de relevante. Sabemos que cobertura é só uma métrica entre várias. Mas com certeza é melhor buscar uma porcentagem alta do que não ter ideia nenhuma de onde você está.
Medindo o sucesso
-
"Velocidade" (roubando isso do Scrum mesmo sem usarmos Scrum)
- Crescimento contínuo nas métricas mensais (funcionalidades, melhorias, refatorações)
-
Qualidade
- Reduzir o esforço de PR gasto com correções dos atuais 35% para 20% ou menos até o fim de 2026 (calculado com base em alterações e adições/exclusões de arquivos)
-
Saúde arquitetural
- Métricas de aderência a padrões, acoplamento entre tecnologias, violações de fronteira
-
Eficiência da revisão
- PRs menores, revisões mais rápidas, menos rodadas de feedback
-
Uptime da aplicação e da API
- Quão perto estamos de 99,99%?
