Engenharia em 2026 e além
Estamos construindo uma infraestrutura que quase nunca pode falhar. Para isso, avançamos rápido enquanto entregamos software incrível, com qualidade, sem atalhos nem concessões. Este documento apresenta os padrões de engenharia que vão nos guiar em 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. Padrões altos na code review
- 3. Puxe uns aos outros para fazer o que é certo
- 4. Mire na simplicidade
- 5. Automatize tudo
- Padrões arquiteturais
- Vertical Slice Architecture: packages/features
- Por que vertical slices importam
- Diretrizes para a Organização das Funcionalidades
- Benefícios
- Padrão Repository e Injeção de Dependência
- Objetos de Transferência de Dados (DTOs)
- Padrões de Domain-Driven Design
- Consistência da base de código
- Leve as condicionais para o ponto de entrada da aplicação
- Por que isso importa
- Como conseguir isso
- Design de API: Controllers enxutos e abstração de HTTP
- Versionamento de API e mudanças incompatíveis
- Performance e complexidade de algoritmos
- Pense em escala desde o primeiro dia
- Escolha as estruturas de dados e algoritmos certos
- Verificações automatizadas 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 consiste em 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 os outros times para estabelecer melhores práticas em toda a organização.
-
Consumer, Enterprise e Platform Teams: Times focados em 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 PRs da comunidade open source, dar feedback e conduzir esse trabalho até o merge. Este time garante que quem contribui para o open source tenha uma ótima experiência e que as contribuições atendam aos nossos padrões de qualidade. Manter uma cultura forte de 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 a forma como construímos software:

Praticamente dobramos nossa produção 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 de correções para funcionalidades, melhorias de performance, refactors e tarefas. Essa mudança mostra que investir em qualidade e arquitetura não faz você andar mais devagar. Faz você acelerar.
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 alcançar 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 da maneira certa desde o primeiro dia. Temos o luxo de implementar esses padrões de engenharia sem a pressão de demandas imediatas do mercado ou limitaçõ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 de dados diretamente para o frontend. Criar abstrações adequadas e injeção de dependência exige mais planejamento 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 refactors depois
-
Alta cobertura de testes evita bugs que, de outra forma, consumiriam semanas de depuração e hotfixes (veja 2023 até meados de 2024)
-
Boas abstrações tornam muito mais rápido adicionar novas funcionalidades ao longo do tempo
-
Limites claros e DTOs evitam a erosão arquitetural que, com o tempo, exige reescritas completas
A Cal.com trajetória mostra o que acontece quando você otimiza para velocidade imediata. Alta velocidade inicial que vai se degradando conforme a dívida técnica se acumula, atalhos arquiteturais criam gargalos e mais tempo é gasto corrigindo problemas do que construindo funcionalidades (veja o gráfico anterior, em que 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 corretamente desde o primeiro dia. Velocidade inicial um pouco mais lenta enquanto estabelecemos os padrões certos, seguida de aceleração exponencial à medida que esses padrões trazem resultados e permitem desenvolvimento mais rápido com maior confiança.
Princípios centrais
1. Sem qualidade adiada
-
Vamos minimizar o "faço isso num PR de follow-up" para pequenos refactors.
- PRs de follow-up para melhorias pequenas raramente acontecem. Em vez disso, eles se acumulam como dívida técnica que nos sobrecarrega meses ou anos depois. Se um pequeno refactor pode ser feito agora, faça agora. Follow-ups devem ser reservados para mudanças substanciais que realmente merecem PRs separados ou para casos excepcionais e urgentes.
2. Padrões altos na code review
-
Não deixe PRs passarem com um monte de ajustes pequenos só para evitar ser "a pessoa chata".
- É exatamente assim que codebases ficam desleixadas com o tempo. Code review 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. Corrija isso antes do merge, não depois.
3. Puxe uns aos outros para fazer o que é certo
-
Nós nos responsabilizamos mutuamente pela qualidade
- Cortar caminho pode parecer mais rápido no momento, mas cria problemas que deixam todo mundo mais devagar depois. Quando você vir um colega prestes a fazer merge de um PR com problemas óbvios, fale. Quando alguém sugerir um hack rápido em vez da solução correta, questione. Quando você estiver tentado a pular testes ou ignorar padrões arquiteturais, espere que seu time te desafie.
-
Não se trata de ser difícil ou atrasar as pessoas
- Trata-se de responsabilidade coletiva pela nossa codebase e pela nossa reputação. Cada atalho que uma pessoa toma vira problema de todo mundo. Cada canto cortado hoje significa mais sessões de depuração, mais hotfixes e mais clientes frustrados amanhã.
-
Faça ser normal questionar decisões ruins, com respeito
- Se alguém disser "vamos só hard-codear isso por enquanto", a resposta esperada deve ser "o que levaria para fazer do jeito certo na primeira vez?" Se alguém quiser commitar código sem testes, o time deve questionar. Se alguém sugerir copiar e colar em vez de criar uma abstração adequada, aponte isso com respeito.
-
Estamos construindo algo que precisa quase nunca 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 por todo o sistema. A gente vence como time ou falha como time.
4. Mire na simplicidade
-
Priorize clareza em vez de esperteza
- O objetivo é um código fácil de ler e entender rapidamente, não uma complexidade elegante. Sistemas simples reduzem a carga cognitiva de 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 foque exclusivamente na lógica de negócio complexa e nas arquiteturas críticas. Times que adotam workflows modernos assistidos por IA estão cada vez mais dependendo de 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 estão ficando cada vez mais no passado. A IA pode montar rapidamente e de forma inteligente mega suites de testes para nós.
-
-
Nosso CI é o chefão final
-
Tudo neste documento de padrões é verificado antes de o código ser mergeado nos PRs
-
Nenhuma surpresa chega ao 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 via 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 vertical slice completa da aplicação, guiada pelo domínio que ela toca.
Estrutura:
packages/features/
availability/
bookings/
ooo/
payments/
routing/
scheduling/
...
Cada pasta de feature é uma vertical slice autocontida que inclui tudo o que é necessário para aquele domínio:
-
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
-
Repositories: Acesso a dados específico das necessidades dessa feature
-
DTOs: Objetos de transferência de dados para atravessar fronteiras
-
Componentes de UI: Componentes frontend relacionados a esta feature (quando aplicável)
-
Testes: Testes unitários, de integração e e2e para esta feature
Por que vertical slices importam
A arquitetura em camadas tradicional 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 mexer em arquivos espalhados por vários diretórios
-
É difícil entender o que uma funcionalidade faz porque o código dela está fragmentado
-
As equipes atrapalham o trabalho umas das outras quando trabalham em funcionalidades diferentes
-
Você não consegue extrair nem 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ê consegue entender toda a funcionalidade de disponibilidade explorando um único diretório
-
As equipes podem trabalhar em funcionalidades diferentes sem conflitos (se a equipe de engenharia da Cal.com crescer, mas muito provavelmente em coss.com vamos fazer com que equipes assumam pacotes maiores)
-
As funcionalidades são fracamente acopladas e podem evoluir de forma independente
Diretrizes para a Organização das Funcionalidades
Em teoria, cada funcionalidade é implantável de forma independente. Embora talvez não façamos deploy delas separadamente de fato, organizar desse jeito nos força a manter as dependências claras e o acoplamento mínimo. Essa é a premissa e o sucesso dos microsserviços, embora ainda não vamos fazer deploy de microsserviços.
As funcionalidades se comunicam por meio de interfaces bem definidas. Se bookings precisa de dados de disponibilidade, ele importa de @calcom/features/availability por meio de interfaces exportadas, e não acessando detalhes internos da implementação.
O código compartilhado vive nos lugares apropriados:
-
Utilitários agnósticos de domínio e preocupações transversais (auth, logging):
packages/lib -
Primitivos de UI compartilhados:
packages/ui(e em breve coss.com ui)
Os limites de domínio são impostos automaticamente. Vamos criar linting que impeça acessar os internos de funcionalidades onde você não deveria poder entrar. Se packages/features/bookings tentar importar de packages/features/availability/services/internal, o linter vai bloquear. Todas as dependências entre funcionalidades precisam 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
-
Facilidade de descoberta
- Procurando a lógica de booking? Ela está toda em
packages/features/bookings. Sem precisar vasculhar 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 está dependendo. 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 o Prisma ilustra isso perfeitamente. Hoje temos referências ao 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 o Prisma para v6.16. Algo que deveria ter sido apenas um refactor localizado atrás de repositories protegidos virou uma busca longa e quase interminável por problemas em vários apps.
O padrão daqui para frente:
-
Todo acesso ao banco de dados precisa passar por classes Repository. Já temos um bom começo 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 mudarmos de Prisma para Drizzle ou outro ORM, as únicas mudanças necessárias são:
-
Implementações de Repository
-
Fiação do container de DI para novos repositories
-
Nada mais no código deve se importar ou mudar
-
Isso não é teórico. É assim que construímos sistemas de fácil manutenção.
Objetos de Transferência de Dados (DTOs)
Tipos do banco de dados não devem vazar para o frontend. Isso virou um atalho popular na nossa stack, mas é um sinal de código 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 é especialmente 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 pelo 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 devolver 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 em DTOs da camada de aplicação, depois transforme 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 de 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 aos dados, isolam escolhas tecnológicas
-
Dependency Injection
- Permite baixo acoplamento, facilita testes, isola preocupações
-
Caching Proxies
-
Envolvem repositories ou services para adicionar comportamento de cache de forma transparente
-
Não é a única forma de fazer caching, 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 devem parecer que uma pessoa as escreveu. Esse nível de consistência exige adesão estrita aos padrões estabelecidos, regras abrangentes de lint que reforcem os padrões arquiteturais, code reviews que rejeitem violações de padrões + a ajuda de revisores de código com IA.
Leve as condicionais para o ponto de entrada da aplicação
Instruções if devem ficar no ponto de entrada, não espalhadas pelos seus services. Este é um dos princípios arquiteturais mais importantes para manter um código limpo e focado, que não escorrega para uma complexidade impossível de manter.
Veja como o código se degrada ao longo do tempo: um service é escrito com um propósito claro e específico. A lógica é limpa e focada. Depois surge um novo requisito de produto, e alguém adiciona uma instrução if. Alguns anos e vários outros requisitos depois, esse service está cheio de verificações condicionais para cenários diferentes. O service virou:
-
Complicado e difícil de ler
-
Difícil de entender e raciocinar sobre ele
-
Mais suscetível a bugs
-
Violando a responsabilidade única (lidando com casos demais)
-
Quase impossível de testar completamente
O service extrapolou suas responsabilidades 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 a services especializados que lidam com sua lógica específica sem condicionais.
Exemplo da nossa base de código:
O BillingPortalServiceFactory determina se o billing é para uma organization, team ou 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();
}
}
Depois, cada service lida com sua lógica específica sem precisar verificar "sou uma org ou uma equipe?":
// 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 conhecer outros contextos. O
OrganizationBillingPortalServicenão contém instruções if verificandoif (isTeam)ouif (isUser). Ele só sabe como lidar com organizações.
- Cada service tem uma responsabilidade e não precisa conhecer outros contextos. O
-
As mudanças ficam isoladas
- Quando você precisar modificar a lógica de cobrança da organização, toque apenas 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 o seu código toma.
- Quando você precisar modificar a lógica de cobrança da organização, toque apenas em
-
Testar é direto
- Teste cada serviço de forma independente com seus cenários específicos. Não há necessidade de testar todas as combinações de condicionais em contextos diferentes.
-
Novos requisitos não poluem 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 permanecem intocados e focados.
- por exemplo, quando você precisa adicionar cobrança enterprise com regras diferentes, você cria
Como conseguir isso
-
Empurre os condicionais para cima, para controllers, factories ou lógica de roteamento. Deixe que esses pontos de entrada decidam qual serviço usar.
-
Mantenha os serviços puros e focados em uma única responsabilidade. Se um serviço precisa verificar "que tipo eu sou?", você provavelmente precisa de vários serviços.
-
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 serviço ganhando condicionais para cenários diferentes, isso é um sinal para refatorar em serviços especializados.
Design de API: Controllers enxutos e abstração de HTTP
-
Controllers são camadas enxutas que lidam apenas com preocupações de HTTP.
- Eles recebem requests, processam-nas e mapeiam dados para DTOs que são passados para a lógica central da aplicação. Daqui em diante, nenhuma lógica de aplicação ou de core deve aparecer em rotas de API ou handlers de tRPC.
-
Precisamos separar a tecnologia HTTP da nossa aplicação.
- A forma como transferimos dados entre client e server (seja REST, tRPC etc.) não deve influenciar como a 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 dos parâmetros da request, body, headers
-
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 respostas 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
-
Realizar transformações complexas de dados ou cálculos
-
Tomar decisões sobre o que a aplicação deve fazer
-
Saber detalhes de implementação do domínio
Exemplo de padrão de controller enxuto:
// 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 incompatíveis
Sem mudanças incompatíveis. Isso é crítico. Depois que um endpoint da API se torna público, ele precisa continuar estável. Mudanças incompatíveis destroem a confiança dos desenvolvedores e criam pesadelos de integração para os nossos usuários.
Estratégias para evitar mudanças incompatíveis:
-
Sempre adicione novos campos como opcionais
-
Use versionamento de API quando precisar mudar o comportamento existente
-
Descontinue endpoints antigos com cuidado e caminhos de migração claros
-
Mantenha compatibilidade com versões anteriores por pelo menos duas versões principais
Quando você precisar fazer mudanças incompatíveis:
-
Crie uma nova versão da API usando o versionamento baseado em data no API v2 (talvez a gente investigue também o versionamento por nome que a Stripe introduziu recentemente)
-
Execute as duas versões simultaneamente durante a transição (já fazemos isso no API v2)
-
Forneça ferramentas automatizadas de migração quando possível
-
Dê tempo suficiente para os usuários migrarem (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 uma escala enterprise. Performance não é algo que a gente otimiza depois. É algo que a gente constrói direito desde o início.
Pense em escala desde o primeiro dia
Ao construir funcionalidades, pergunte sempre: "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 mapeamentos aninhados sobre listas grandes
Exemplo do mundo real: Para 100 slots disponíveis e 50 períodos ocupados, um algoritmo O(n²) faz 5.000 verificações. Leve 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 ao escolher 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 bater
-
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 scheduling, disponibilidade e consultas de intervalo, use estruturas de árvore adequadas em vez de comparação forçada
Exemplo de transformação:
// 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 automatizadas 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
-
Várias chamadas aninhadas de
.some,.findou.filter -
Recursão sem memoização
-
Anti-padrões conhecidos para o nosso domínio (scheduling, verificações de disponibilidade etc.)
Benchmarks de performance no CI que:
-
Executam algoritmos críticos em 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 de caminhos críticos
-
Dispara alertas quando os algoritmos ficam mais lentos à medida que os dados crescem
-
Detecta regressões antes que os usuários percebam
-
Fornece dados reais de performance para orientar otimizações
Performance é um recurso
O desempenho 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 deve:
-
Perfilhe seu código antes de otimizar, mas pense em complexidade desde o início
-
Teste com dados realistas e em grande escala (não só 5 registros de teste). Já temos scripts de seed prontos. Provavelmente precisamos estendê-los.
-
Escolha algoritmos e estruturas de dados eficientes desde o começo
-
Fique atento a iterações aninhadas na revisão de código
-
Questione 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 janelas de tempo cresce, a complexidade computacional pode explodir exponencialmente. A maioria dos algoritmos ideais de agendamento 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 para uma reunião com 10 pessoas em 3 fusos horários, com restrições individuais de disponibilidade, é computacionalmente caro
-
Adicionar detecção de conflitos, buffers e uma série de outras opções amplia o problema
-
Más escolhas de algoritmo que funcionam bem para equipes pequenas se tornam completamente inutilizáveis para organizações grandes
-
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:
-
Use algoritmos de aproximação que encontrem soluções "boas o suficiente" rapidamente, em vez de soluções perfeitas lentamente
-
Implemente caching agressivo de agendas e disponibilidade calculadas
-
Pré-calcule cenários comuns fora dos horários de pico
-
Quebre grandes problemas de agendamento em partes menores e mais gerenciáveis
-
Defina limites de timeout razoáveis e, quando necessário, faça fallback para algoritmos mais simples
É por isso que desempenho não é só um diferencial em software de agendamento. É a base que determina se o seu sistema consegue escalar para as necessidades enterprise ou entra em colapso sob padrões de uso do mundo real.
Requisitos de cobertura de código
-
Rastreamento global de cobertura
- Acompanhamos a cobertura geral da base de código como uma métrica-chave que melhora ao longo do 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
- Todo PR deve ter cobertura de testes de quase 80%+ para o código que ele 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 mudanças precisam ser testadas. Isso é cobertura geral de testes. A cobertura de testes unitários precisa ficar perto de 100%, especialmente com a possibilidade de usar IA para ajudar a gerar esses testes.
Lidando com o argumento de que "cobertura não conta a história toda": Sim, sabemos que cobertura não garante testes perfeitos. Sabemos que você pode escrever testes sem sentido que atingem cada linha, mas não testam nada relevante. Sabemos que cobertura é só uma métrica entre várias. Mas é certamente melhor buscar uma porcentagem alta do que não ter ideia nenhuma de onde você está.
Medindo o sucesso
-
"Velocidade" (pegando emprestado do Scrum, embora não vamos usar Scrum)
- Crescimento contínuo nas estatísticas mensais (recursos, melhorias, refatorações)
-
Qualidade
- Reduzir o esforço de PR gasto em correções dos atuais 35% para 20% ou menos até o fim de 2026 (calculado com base em alterações e adições/remoções de arquivos)
-
Saúde arquitetural
- Métricas sobre aderência a padrões, acoplamento tecnológico, violações de fronteira
-
Eficiência de revisão
- PRs menores, revisões mais rápidas, menos rodadas de feedback
-
Uptime da aplicação e da API
- O quão perto estamos de 99.99%?
