Soluções

Empresa

Cal.ai

Desenvolvedor

Recursos

Preços

Soluções

Empresa

Cal.ai

Preços

Por

Keith Williams

Engenharia em 2026 e além

Estamos a construir uma infraestrutura que deve quase nunca falhar. Para alcançar isso, movemo-nos rapidamente enquanto lançamos software de qualidade incrível, sem atalhos ou compromissos. Este documento delineia os padrões de engenharia que nos guiarão até 2026 e além.

Estrutura de Equipas

A nossa organização de engenharia é composta por cinco equipas principais, cada uma com responsabilidades distintas:

  • Equipa de Base (Foundation Team): Foca-se em estabelecer e manter padrões de codificação e padrões de arquitetura. Esta equipa trabalha em colaboração com outras equipas para estabelecer as melhores práticas em toda a organização.

  • Equipas de Consumidor, Enterprise e Plataforma: Equipas focadas no produto que lançam funcionalidades rapidamente, mantendo ao mesmo tempo os padrões de qualidade definidos neste documento. Estas equipas provam que a velocidade e a qualidade não se excluem mutuamente.

  • Equipa Comunitária: Responsável por rever rapidamente os PRs da comunidade open source, fornecer feedback e encaminhar esse trabalho até à junção (merge). Esta equipa garante que os nossos contribuidores open source tenham uma excelente experiência e que as suas contribuições cumpram os nossos padrões de qualidade. Manter uma cultura forte de open-source continua a moldar a forma como o Cal.com aborda a colaboração em engenharia e o desenvolvimento de produtos.

Os Nossos Resultados Até Agora

Os dados falam por si. Ao longo do último ano e meio, transformámos fundamentalmente a forma como construímos software:

Screenshot 2025-10-01 at 9.40.36 PM.png

Aproximadamente duplicámos o nosso rendimento de engenharia e, simultaneamente, melhorámos a qualidade. Ainda mais impressionante é a mudança no que estamos a construir:

Screenshot 2025-10-01 at 9.41.51 PM.png

Realocámos com sucesso cerca de 20% do esforço de engenharia de correções para novas funcionalidades, melhorias de desempenho, refatorizações e tarefas de rotina. Esta mudança demonstra que investir em qualidade e arquitetura não o atrasa. Pelo contrário, acelera-o.

A Base do Cal.com Fornece Excelência para o coss.com

  • Cal.com é um negócio estável e lucrativo que continuaremos a fazer crescer.

    • Este sucesso dá-nos uma vantagem única à medida que construímos o coss.com. Ao contrário dos primeiros dias do Cal.com, onde precisávamos de nos mover rapidamente para estabelecer a adequação do produto ao mercado e construir um negócio sustentável, o coss.com começa a partir de uma posição de força.

  • Não precisamos de apressar o coss.com.

    • A estabilidade do Cal.com significa que nos podemos dar ao luxo de construir o coss.com da forma correta desde o primeiro dia. Temos o luxo de implementar estes padrões de engenharia sem a pressão de exigências imediatas do mercado ou restrições de financiamento. Esta é uma posição inicial fundamentalmente diferente.

  • A "lentidão" é um investimento, não um custo.

    • Sim, seguir estes padrões pode parecer mais lento inicialmente e pode até ser frustrante para alguns engenheiros. Escrever DTOs leva mais tempo do que passar tipos de base de dados diretamente para o frontend. Criar abstrações adequadas e injeção de dependências exige mais design inicial. Manter uma cobertura de testes superior a 80% para código novo exige disciplina. Mas esta aparente lentidão é temporária, e o retorno é exponencial.

Considere os retornos compostos...

  • O código que é arquitetado corretamente desde o início não precisa de refatorizações maciças mais tarde

  • Uma elevada cobertura de testes previne bugs que de outra forma consumiriam semanas de depuração e correções urgentes (ver de 2023 a meados de 2024)

  • As abstrações adequadas tornam a adição de novas funcionalidades dramaticamente mais rápida ao longo do tempo

  • Limites claros e DTOs previnem a erosão arquitetónica que eventualmente exige reescritas completas

A trajetória do Cal.com mostra o que acontece quando se otimiza para a velocidade imediata. Alta velocidade inicial que se degrada gradualmente à medida que a dívida técnica se acumula, atalhos arquitetónicos criam gargalos e mais tempo é gasto a corrigir problemas do que a construir funcionalidades (ver gráfico anterior onde estávamos a gastar 55-60% do esforço de engenharia em correções).

A trajetória do coss.com irá abraçar o poder de construir corretamente desde o primeiro dia. Uma velocidade inicial ligeiramente mais lenta enquanto se estabelecem os padrões adequados, seguida por uma aceleração exponencial à medida que esses padrões dão dividendos e permitem um desenvolvimento mais rápido com maior confiança.

Princípios Fundamentais

1. Sem qualidade diferida

  • Minimizaremos o "farei isso num PR de acompanhamento" para pequenas refatorizações.

    • Os PRs de acompanhamento para pequenas melhorias raramente se materializam. Em vez disso, acumulam-se como dívida técnica que nos sobrecarrega meses ou anos mais tarde. Se uma pequena refatorização puder ser feita agora, faça-a agora. Os acompanhamentos devem ser reservados para mudanças substanciais que genuinamente justifiquem PRs separados ou para casos excecionais e urgentes.

2. Altos padrões na revisão de código

  • Não deixe passar PRs com muitas pequenas objeções (nits) apenas para evitar ser "a pessoa má".

    • É precisamente assim que as bases de código se tornam descuidadas ao longo do tempo. A revisão de código não se trata de ser simpático. Trata-se de manter os padrões de qualidade que a nossa infraestrutura exige. Cada pequena correção importa. Cada violação de padrão importa. Resolva-as antes de fazer a junção (merge), não depois.

3. Impulsionar-se mutuamente a fazer a coisa certa

  • Responsabilizamo-nos mutuamente pela qualidade

    • Cortar caminhos pode parecer mais rápido no momento, mas cria problemas que atrasam toda a gente mais tarde. Quando vir um colega de equipa prestes a fundir um PR com problemas óbvios, fale. Quando alguém sugerir um truque rápido em vez da solução adequada, conteste. Quando se sentir tentado a saltar testes ou a ignorar padrões arquitetónicos, espere que os seus colegas o desafiem.

  • Isto não é sobre ser difícil ou atrasar as pessoas

    • Trata-se de propriedade coletiva da nossa base de código e da nossa reputação. Cada atalho que uma pessoa toma torna-se um problema de todos. Cada caminho cortado hoje significa mais sessões de depuração, mais correções urgentes e mais clientes frustrados amanhã.

  • Tornar normal a contestação de más decisões, de forma respeitosa

    • Se alguém disser "vamos apenas codificar isto de forma rígida (hard-code) por agora", a resposta esperada deve ser "o que seria necessário para fazer da forma correta à primeira?". Se alguém quiser submeter código não testado, a equipa deve contestar. Se alguém sugerir copiar e colar em vez de criar uma abstração adequada, aponte isso de forma respeitosa.

  • Estamos a construir algo que não pode quase nunca falhar

    • Esse nível de fiabilidade não acontece por acaso. Acontece quando cada engenheiro se sente responsável pela qualidade, não apenas do seu próprio código, mas de todo o sistema. Vencemos como equipa ou falhamos como equipa.

4. Procurar a simplicidade

  • Priorizar a clareza em detrimento da astúcia

    • O objetivo é obter um código que seja fácil de ler e compreender rapidamente, e não uma complexidade elegante. Sistemas simples reduzem a carga cognitiva de cada engenheiro.

  • Faça a si próprio as perguntas certas

    • Estou realmente a resolver o problema em questão?

    • Estou a pensar demasiado em possíveis casos de utilização futuros?

    • Considerei pelo menos mais 1 alternativa para resolver isto? Como se compara?

  • Simples não significa sem funcionalidades

    • Só porque o nosso objetivo é criar sistemas simples, isso não significa que devam parecer anémicos e sem funcionalidades óbvias.

5. Automatizar tudo

  • Aproveitar a IA

    • Gerar 80% de código padrão (boilerplate) e não crítico utilizando IA, permitindo focarmo-nos exclusivamente em lógica de negócio complexa e arquiteturas críticas. As equipas que adotam fluxos de trabalho modernos assistidos por IA dependem cada vez mais de ferramentas como os melhores servidores MCP para melhorar a produtividade e a orquestração dos programadores.

    • Construir alertas sem ruído e tratamento de erros inteligente.

    • O teste manual pertence cada vez mais ao passado. A IA pode construir de forma rápida e inteligente mega suítes de testes para nós.

  • O nosso CI é o chefe final

    • Tudo o que consta neste documento de padrões é verificado antes de o código ser fundido nos PRs

    • Não entram surpresas no branch principal

    • As verificações são rápidas e úteis

Padrões Arquitetónicos

Estamos a transitar para um modelo arquitetónico rigoroso baseado em Vertical Slice Architecture e Domain-Driven Design (DDD). Os seguintes padrões e princípios serão aplicados rigorosamente nas revisões de PR e através de análise estática (linting).

Vertical Slice Architecture: packages/features

A nossa base de código está organizada por domínio, não por camada técnica. O diretório packages/features é o coração desta abordagem arquitetónica. Cada pasta no seu interior representa uma fatia vertical completa da aplicação, impulsionada pelo domínio que toca.

Estrutura:




Cada pasta de funcionalidade (feature) é uma fatia vertical auto-suficiente que inclui tudo o que é necessário para esse domínio:

  • Lógica de domínio: As regras de negócio fundamentais e entidades específicas desse recurso

  • Serviços de aplicação: Orquestração de casos de utilização para esse domínio

  • Repositórios: Acesso a dados específico para as necessidades dessa funcionalidade

  • DTOs: Objetos de transferência de dados para cruzar fronteiras

  • Componentes de UI: Componentes do frontend relacionados com esta funcionalidade (onde aplicável)

  • Testes: Testes unitários, de integração e e2e para esta funcionalidade

Por que as Fatias Verticais Importam

A arquitetura em camadas tradicional organiza por preocupações técnicas:




Isto cria vários problemas:

  • Mudanças numa funcionalidade exigem alterar ficheiros espalhados por múltiplos diretórios

  • É difícil perceber o que uma funcionalidade faz porque o seu código está fragmentado

  • As equipas interferem no trabalho umas das outras ao trabalhar em funcionalidades diferentes

  • Não se consegue extrair ou descontinuar uma funcionalidade facilmente

A arquitetura de fatias verticais organiza por domínio:




Isto resolve estes problemas:

  • Tudo o que se relaciona com disponibilidade vive em packages/features/availability

  • Pode compreender toda a funcionalidade de disponibilidade explorando apenas um diretório

  • As equipas podem trabalhar em funcionalidades diferentes sem conflitos (se a equipa de engenharia do Cal.com crescer, mas com toda a certeza no coss.com teremos equipas a assumir pacotes importantes)

  • As funcionalidades são fracamente acopladas e podem evoluir de forma independente

Diretrizes para Organização de Funcionalidades

Teoricamente, cada funcionalidade é implementável de forma independente. Embora possamos não as implementar separadamente na realidade, organizar desta forma força-nos a manter as dependências claras e o acoplamento mínimo. Esta é a premissa de sucesso dos microsserviços, embora ainda não estejamos a implementar microsserviços.

As funcionalidades comunicam através de interfaces bem definidas. Se as reservas precisarem de dados de disponibilidade, importam de @calcom/features/availability através de interfaces exportadas, e não acedendo a detalhes de implementação interna.

O código partilhado reside nos locais apropriados:

  • Utilitários agnósticos de domínio e preocupações transversais (autenticação, registo de logs): packages/lib

  • Primitivas de UI partilhadas: packages/ui (e em breve coss.com ui)

Os limites do domínio são aplicados automaticamente. Construiremos linters para evitar o acesso a detalhes internos de funcionalidades às quais não deve ser permitido aceder. Se packages/features/bookings tentar importar de packages/features/availability/services/internal, o linter irá bloqueá-lo. Todas as dependências entre funcionalidades devem passar pela API pública da funcionalidade.

good_architecture_diagram.pngbad_architecture_diagram.png

Novas funcionalidades começam como fatias verticais. Ao construir algo novo, crie uma nova pasta em packages/features com a fatia vertical completa. Isto torna claro o que está a construir e mantém tudo organizado desde o primeiro dia.

Benefícios

  • Facilidade de Descoberta

    • Procura lógica de reservas? Está tudo em packages/features/bookings. Não há necessidade de procurar em controladores, serviços, repositórios e utilitários espalhados pela base de código.

  • Testes mais fáceis

    • Teste a funcionalidade completa como uma unidade. Tem todas as peças num só local, tornando os testes de integração naturais e descomplicados.

  • Dependências mais claras

    • Quando vê import { getAvailability } from '@calcom/features/availability', sabe exatamente de que funcionalidade está a depender. Quando as dependências crescem e se tornam demasiado complexas, isso fica óbvio e pode ser resolvido.

Padrão Repositório e Injeção de Dependências

As escolhas tecnológicas não devem infiltrar-se na aplicação. O problema do Prisma ilustra isto perfeitamente. Atualmente, temos referências ao Prisma espalhadas por centenas de ficheiros. Isto cria um acoplamento maciço e torna as mudanças de tecnologia proibitivamente caras. Estamos a sentir a dor disso agora ao atualizar o Prisma para a v6.16. Algo que deveria ter sido apenas uma refatorização localizada atrás de repositórios protegidos tem sido uma busca sinuosa e quase interminável de problemas em várias aplicações.

O padrão daqui para a frente:

  • Todo o acesso à base de dados deve passar por classes Repositório. Já temos um bom avanço em relação a isto.

  • Os repositórios são o único código que sabe sobre o Prisma (ou qualquer outro ORM). Nenhuma lógica deve residir neles.

  • Os repositórios são injetados através de contentores de Injeção de Dependências (DI)

  • Se alguma vez mudarmos do Prisma para o Drizzle ou outro ORM, as únicas mudanças necessárias serão:

    • Implementações de repositório

    • Configuração do contentor de DI para novos repositórios

    • Mais nada na base de código se deve importar ou mudar

Isto não é teórico. É assim que construímos sistemas sustentáveis.

Objetos de Transferência de Dados (DTOs)

Os tipos da base de dados não devem chegar ao frontend. Este tornou-se um atalho popular na nossa pilha tecnológica, mas é um sintoma de mau código (code smell) que cria múltiplos problemas.

  • Acoplamento tecnológico (tipos de Prisma acabam em componentes React)

  • Riscos de segurança (fuga acidental de campos confidenciais)

  • Contratos frágeis entre servidor e cliente (isto é particularmente problemático à medida que construímos muito mais APIs)

  • Incapacidade de evoluir o esquema da base de dados de forma independente

  • Todas as conversões de DTOs através de Zod, mesmo para respostas de API, para garantir que todos os dados são validados antes de serem enviados para o utilizador. É melhor falhar do que devolver algo errado.

O padrão daqui para a frente:
Criar DTOs explícitos em cada limite arquitetónico.

  1. Camada de dados → Camada de aplicação → API: Transformar modelos de base de dados em DTOs da camada de aplicação e, em seguida, transformar DTOs da aplicação em DTOs específicos da API

  2. API → Camada de aplicação → Camada de dados: Transformar DTOs de API através da camada de aplicação em DTOs específicos de dados

Sim, isto exige mais código. Sim, vale a pena. Limites explícitos previnem a erosão arquitetónica que cria pesadelos de manutenção a longo prazo.

Padrões de Domain-Driven Design

Os seguintes padrões devem ser utilizados de forma correta e consistente:

  • Serviços de Aplicação

    • Orquestrar casos de utilização, coordenar entre serviços de domínio e repositórios

  • Serviços de Domínio

    • Conter lógica de negócio que não pertence naturalmente a uma única entidade

  • Repositórios

    • Abstrair o acesso a dados, isolar escolhas tecnológicas

  • Injeção de Dependências

    • Permitir acoplamento fraco, facilitar testes, isolar preocupações

  • Proxies de Caching

    • Envolver repositórios ou serviços para adicionar comportamento de cache de forma transparente

    • Não é a única forma de fazer cache, claro, mas um excelente ponto de partida

  • Decorators

    • Adicionar preocupações transversais (logs, métricas, etc.) sem poluir a lógica de domínio

Consistência da Base de Código

As nossas bases de código devem dar a sensação de terem sido escritas por uma única pessoa. Este nível de consistência exige uma adesão estrita aos padrões estabelecidos, regras de linting abrangentes que imponham padrões arquitetónicos, revisões de código que rejeitem violações de padrões e a ajuda de revisores de código de IA.

Mover Condicionais para o Ponto de Entrada da Aplicação

As instruções "if" pertencem ao ponto de entrada, não distribuídas pelos seus serviços. Este é um dos princípios arquitetónicos mais importantes para manter o código limpo, focado e livre de complexidade insustentável.

Eis como o código se degrada ao longo do tempo: Um serviço é escrito para um propósito claro e específico. A lógica é limpa e focada. Em seguida, surge um novo requisito de produto e alguém adiciona uma instrução "if". Alguns anos e mais alguns requisitos depois, esse serviço fica repleto de verificações condicionais para diferentes cenários. O serviço tornou-se:

  • Complicado e difícil de ler

  • Difícil de compreender e raciocinar sobre

  • Mais suscetível a bugs

  • Umas violação de responsabilidade única (tratando de demasiados casos diferentes)

  • Quase impossível de testar exaustivamente

O serviço ultrapassou os seus limites em termos de responsabilidades e lógica.

Uma Solução: Padrão Factory com Serviços Especializados
Utilize o padrão factory para tomar decisões no ponto de entrada e, em seguida, delegue para serviços especializados que tratam da sua lógica específica sem condicionais.

Exemplo da nossa base de código:
O BillingPortalServiceFactory determina se a faturação é para uma organização, equipa ou utilizador individual e, em seguida, devolve o serviço apropriado:

export class BillingPortalServiceFactory {  
  static async createService(teamId: number): Promise<BillingPortalService>

export class BillingPortalServiceFactory {  
  static async createService(teamId: number): Promise<BillingPortalService>

export class BillingPortalServiceFactory {  
  static async createService(teamId: number): Promise<BillingPortalService>

Cada serviço passa a tratar da sua lógica específica sem precisar de verificar "sou uma organização ou uma equipa?":

// 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]

// 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]

// 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]

Porque é que Isto Importa

  • Os serviços mantêm-se focados

    • Cada serviço tem uma única responsabilidade e não precisa de conhecer outros contextos. O OrganizationBillingPortalService não contém instruções "if" a verificar if (isTeam) ou if (isUser). Apenas sabe lidar com organizações.

  • As alterações são isoladas

    • Quando precisa de modificar a lógica de faturação da organização, apenas mexe em OrganizationBillingPortalService. Não corre o risco de quebrar a faturação da equipa ou do utilizador. Não precisa de rastrear condicionais aninhados para perceber qual o caminho que o seu código segue.

  • Os testes são diretos

    • Teste cada serviço individualmente com os seus cenários específicos. Não há necessidade de testar cada combinação de condicionais em diferentes contextos.

  • Novos requisitos não poluem o código existente

    • ex. Quando precisar de adicionar faturação enterprise com regras diferentes, cria o EnterpriseBillingPortalService. A factory ganha mais uma condicional, mas os serviços existentes permanecem intocados e focados.

Como alcançar

  • Empurre as condicionais para controladores, factories ou lógica de encaminhamento (routing). Deixe que estes pontos de entrada tomem decisões sobre qual serviço utilizar.

  • Mantenha os serviços puros e focados numa única responsabilidade. Se um serviço precisa de verificar "que tipo sou eu?", provavelmente precisa de múltiplos serviços.

  • Prefira polimorfismo em vez de condicionais

    • As interfaces definem o contrato. As implementações concretas fornecem os detalhes específicos.

  • Atenção à acumulação de instruções "if"

    • Durante a revisão de código, se vir um serviço a ganhar condicionais para cenários diferentes, isso é um sinal de que deve refatorá-lo em serviços especializados.

Design de API: Controladores Finos e Abstração HTTP

  • Os controladores são camadas finas que tratam apenas de questões HTTP.

    • Eles recebem pedidos, processam-nos e mapeiam os dados para DTOs que são passados para a lógica principal da aplicação. Daqui para a frente, nenhuma aplicação ou lógica principal deve ser vista em rotas de API ou manipuladores (handlers) tRPC.

  • Devemos desvincular a tecnologia HTTP da nossa aplicação.

    • A forma como transferimos dados entre o cliente e o servidor (seja REST, tRPC, etc.) não deve influenciar o funcionamento do núcleo da nossa aplicação. O HTTP é um mecanismo de entrega, não um direcionador arquitetónico.

Responsabilidades do controlador (e APENAS estas):

  • Receber e validar pedidos recebidos

  • Extrair dados de parâmetros do pedido, corpo, cabeçalhos

  • Transformar dados do pedido em DTOs

  • Chamar os serviços de aplicação apropriados com esses DTOs

  • Transformar as respostas do serviço de aplicação em DTOs de resposta

  • Devolver respostas HTTP com os códigos de estado corretos

Os controladores NÃO devem:

  • Conter lógica de negócio ou regras de domínio

  • Aceder diretamente a bases de dados ou serviços externos

  • Realizar transformações ou cálculos de dados complexos

  • Tomar decisões sobre o que a aplicação deve fazer

  • Saber sobre detalhes de implementação do domínio

Exemplo do padrão de controlador fino (thin controller):




Versões de API e Alterações Incompatíveis

Sem alterações incompatíveis (breaking changes). Isto é crítico. Uma vez que um endpoint de API é público, ele deve permanecer estável. Alterações incompatíveis destroem a confiança do programador e criam pesadelos de integração para os nossos utilizadores.

Estratégias para evitar alterações incompatíveis:

  • Adicionar sempre novos campos como opcionais

  • Usar controlo de versões de API quando for estritamente necessário alterar o comportamento existente

  • Descontinuar endpoints antigos de forma harmoniosa com caminhos de migração claros

  • Manter a compatibilidade com versões anteriores por pelo menos duas versões principais

Quando tiver de fazer alterações incompatíveis:

  • Crie uma nova versão de API utilizando o controlo de versões por data na API v2 (talvez analisemos também a versão nomeada que a Stripe introduziu recentemente)

  • Execute ambas as versões simultaneamente durante a transição (já fazemos isso na API v2)

  • Forneça ferramentas de migração automatizadas sempre que possível

  • Dê aos utilizadores tempo suficiente para migrar (mínimo de 6 meses para APIs públicas)

  • Documente exatamente o que mudou e porquê

Desempenho e Complexidade de Algoritmos

Nós construímos para grandes organizações e equipas. O que funciona bem com 10 utilizadores ou 50 registos pode colapsar sob o peso da escala empresarial. O desempenho não é algo que otimizamos mais tarde. É algo que construímos corretamente desde o início.

Pense na Escala Desde o Primeiro Dia

Ao construir funcionalidades, pergunte sempre: "Como se comporta isto com 1.000 utilizadores? 10.000 registos? 100.000 operações?" A diferença entre algoritmos O(n) e O(n²) pode ser impercetível em desenvolvimento, mas catastrófica em produção.

Padrões O(n²) comuns a evitar:

  • Iterações de matrizes (arrays) aninhadas (.map dentro de .map, .forEach dentro de .forEach)

  • Métodos de matriz como .some, .find ou .filter dentro de ciclos ou callbacks

  • Verificar cada elemento contra todos os outros elementos sem otimização

  • Filtros encadeados ou mapeamentos aninhados em listas grandes

Exemplo do mundo real: Para 100 slots disponíveis e 50 períodos ocupados, um algoritmo O(n²) executa 5.000 verificações. Dimensione isso para 500 slots e 200 períodos ocupados, e estará a fazer 100.000 operações. Trata-se de um aumento de 20x na carga de processamento para apenas um aumento de 5x nos dados.

Escolha as Estruturas de Dados e Algoritmos Corretos

A maioria dos problemas de desempenho resolve-se escolhendo as melhores estruturas de dados e algoritmos:

  • Ordenação + saída antecipada: Ordene os seus dados uma vez e depois saia dos ciclos (break) quando souber que os restantes elementos não corresponderão

  • Procura binária: Utilize a pesquisa binária para consultas em matrizes ordenadas em vez de varreduras lineares

  • Técnicas de dois ponteiros: Para fundir ou intersetar sequências ordenadas, percorra ambas com ponteiros em vez de ciclos aninhados

  • Mapas/conjuntos de hash (Hash maps/sets): Utilize objetos ou Sets para consultas O(1) em vez de .find ou .includes em matrizes

  • Árvores de intervalos: Para agendamento, disponibilidade e consultas de intervalos, utilize estruturas de árvores adequadas em vez de comparações por força bruta

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]

// 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]

// 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]

Verificações de Desempenho Automatizadas

Iremos implementar múltiplas camadas de defesa contra regressões de desempenho:

Regras de linting que sinalizam:

  • Funções com ciclos aninhados ou métodos de matriz aninhados

  • Chamadas múltiplas e aninhadas de .some, .find ou .filter

  • Recursividade sem memoization (mecanismo de memorização)

  • Antipadrões conhecidos para o nosso domínio (agendamento, verificações de disponibilidade, etc.)

Testes de referência (benchmarks) de desempenho em CI que:

  • Executam algoritmos críticos em dados realistas e em larga escala

  • Comparam tempos de execução com a linha de base em cada PR

  • Bloqueiam junções (merges) que introduzam regressões de desempenho

  • Testam com dados à escala empresarial (milhares de utilizadores, dezenas de milhares de registos)

Monitorização em produção que:

  • Rastreia o tempo de execução de caminhos críticos

  • Alerta quando os algoritmos abrandam com o crescimento dos dados

  • Identifica regressões de desempenho antes que os utilizadores se apercebam

  • Fornece dados de desempenho reais para fundamentar otimizações

Desempenho é uma Funcionalidade

O desempenho não é opcional. Não é algo que deixamos "para mais tarde." Para clientes empresariais que agendam reuniões com grandes equipas, respostas lentas significam perda de produtividade e utilizadores frustrados (a nossa experiência com alguns clientes empresariais de maior dimensão serve de testemunho disto).

Cada engenheiro deve:

  • Analisar o perfil do código (profile) antes de o otimizar, mas pensar na complexidade desde o início

  • Testar com dados realistas e em larga escala (e não apenas com 5 registos de teste). Já temos scripts de geração (seed) de dados criados. Provavelmente precisamos de os expandir.

  • Escolher algoritmos e estruturas de dados eficazes à partida

  • Prestar atenção 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-Completa (NP-Hard) do Agendamento

Os problemas de agendamento são fundamentalmente complexos (NP-hard). Isto significa que à medida que o número de restrições, participantes ou slots de tempo cresce, a complexidade computacional pode explodir exponencialmente. A maioria dos algoritmos de agendamento ideais tem uma complexidade de tempo exponencial no pior caso, tornando a escolha do algoritmo absolutamente crítica.

Implicações no mundo real:

  • Encontrar o horário ideal para reuniões para 10 pessoas em 3 fusos horários com restrições de disponibilidade individuais é computacionalmente caro

  • Adicionar deteção de conflitos, buffers e uma imensidão de outras opções amplia o problema

  • Piores escolhas de algoritmos que funcionam bem para equipas pequenas tornam-se completamente inutilizáveis para grandes organizações

  • O que demora milissegundos para 5 utilizadores pode demorar muitos segundos para organizações

Estratégias para gerir a complexidade NP-Hard:

  • Utilizar algoritmos de aproximação que encontram soluções "suficientemente boas" rapidamente em vez de soluções perfeitas de forma lenta

  • Implementar caching agressivo de agendas e disponibilidades já calculadas

  • Pré-calcular cenários comuns durante as horas de menor atividade

  • Dividir problemas de agendamento grandes em partes menores e mais fáceis de gerir

  • Definir limites de tempo limite (timeout) razoáveis e recorrer a algoritmos mais simples quando necessário

É por isso que o desempenho não é apenas um aspeto agradável num software de agendamento. É a base que determina se o seu sistema consegue escalar para as necessidades empresariais ou se colapsa sob padrões reais de utilização.

Requisitos de Cobertura de Código

  • Rastreio de cobertura global

    • Acompanhamos a cobertura geral da base de código como uma métrica chave que melhora com o tempo. Isso dá-nos visibilidade sobre o nível de maturação dos nossos testes e ajuda a identificar áreas que necessitam de atenção. A percentagem de cobertura global é mostrada em destaque nos nossos dashboards.

  • Cobertura superior a 80% para código novo

    • Cada PR deve ter uma cobertura de testes próxima ou superior a 80% para o código que introduz ou modifica. Isto é controlado automaticamente no nosso pipeline de CI. Se adicionar 50 linhas de código novo, essas 50 linhas têm de ser cobertas por testes. Se modificar uma função existente, as suas alterações têm de ser testadas. Trata-se da cobertura global de testes. A cobertura de testes unitários precisa de estar perto de 100%, especialmente com a possibilidade de recorrer a IA para ajudar a gerá-los.

Abordando o argumento de que "a cobertura não conta toda a história": Sim, sabemos que a cobertura de código não garante testes perfeitos. Sabemos que é possível escrever testes sem sentido que passam por todas as linhas mas não testam nada de útil. Sabemos que a cobertura é apenas uma métrica entre muitas. No entanto, é sem dúvida melhor apontar para uma percentagem elevada do que não ter qualquer ideia de onde nos encontramos.

Medir o Sucesso

  • "Velocidade" (termo emprestado do Scrum mesmo sem utilizarmos Scrum)

    • Crescimento contínuo nas estatísticas mensais (funcionalidades, melhorias, refatorizações)

  • Qualidade

    • Reduzir o esforço de PR dedicado a correções dos atuais 35% para 20% ou menos até ao final de 2026 (calculado com base nas alterações e adições/remoções de ficheiros)

  • Saúde arquitetónica

    • Métricas de adesão a padrões, acoplamento de tecnologia, violações de limites

  • Eficiência de revisão

    • PRs mais pequenos, revisões mais rápidas, menos rondas de feedback

  • Disponibilidade (uptime) de aplicação e API

    • Quão perto estamos de 99,99%?

Comece com o Cal.com gratuitamente hoje!

Experimente uma programação e produtividade sem interrupções, sem taxas ocultas. Registe-se em segundos e comece a simplificar a sua programação hoje, sem necessidade de cartão de crédito!