Engenharia em 2026 e além
Estamos a construir infraestrutura que quase nunca pode falhar. Para o conseguir, movemo-nos depressa enquanto lançamos software com qualidade incrível, sem atalhos nem compromissos. Este documento descreve os padrões de engenharia que nos vão guiar em 2026 e além.

On this page
- Estrutura da equipa
- Os nossos resultados até agora
- A base da Cal.com permite excelência para coss.com
- Princípios centrais
- 1. Sem qualidade adiada
- 2. Padrões elevados na code review
- 3. Puxa pelos outros para fazerem a coisa certa
- 4. Procura a simplicidade
- 5. Automatiza tudo
- Padrões arquitetónicos
- Vertical Slice Architecture: packages/features
- Porque é que as vertical slices importam
- Diretrizes para a Organização de Funcionalidades
- Benefícios
- Padrão de Repositório e Injeção de Dependência
- Objectos de Transferência de Dados (DTOs)
- Padrões de Design Orientado ao Domínio
- Consistência da Base de Código
- Move as condicionais para o ponto de entrada da aplicação
- Porque é que isto importa
- Como conseguir
- Design da API: Controllers leves e abstração HTTP
- Versionamento da API e mudanças disruptivas
- Performance e complexidade algorítmica
- Pensa na escala desde o primeiro dia
- Escolhe as estruturas de dados e algoritmos certos
- Verificações automáticas de performance
- Performance é uma funcionalidade
- A realidade NP-hard do agendamento
- Requisitos de cobertura de código
- Medir o sucesso
Estrutura da equipa
A nossa organização de engenharia é composta por cinco equipas centrais, cada uma com responsabilidades distintas:
-
Foundation Team: Foca-se em estabelecer e manter padrões de código e padrões arquitetónicos. Esta equipa trabalha em colaboração com outras equipas para estabelecer boas práticas em toda a organização.
-
Consumer, Enterprise, and Platform Teams: Equipas focadas no produto que lançam funcionalidades rapidamente, mantendo os padrões de qualidade definidos neste documento. Estas equipas provam que velocidade e qualidade não são mutuamente exclusivas.
-
Community Team: Responsável por rever rapidamente PRs da comunidade open source, dar feedback e acompanhar esse trabalho até à merge. Esta equipa garante que os nossos contribuidores de open source têm uma ótima experiência e que as suas contribuições cumprem os nossos padrões de qualidade. Manter uma forte cultura de open-source continua a moldar a forma como a Cal.com aborda a colaboração de engenharia e o desenvolvimento de produto.
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:

Aproximadamente duplicámos a nossa produção de engenharia enquanto, ao mesmo tempo, melhorámos a qualidade. Ainda mais impressionante é a mudança no que estamos a construir:

Conseguimos realocar cerca de 20% do esforço de engenharia para longe de correções e para funcionalidades, melhorias de desempenho, refactors e tarefas. Esta mudança demonstra que investir em qualidade e arquitetura não te abranda. Acelera-te.
A base da Cal.com permite excelência para coss.com
-
Cal.com é um negócio estável e rentável que vamos continuar a fazer crescer.
- Este sucesso dá-nos uma vantagem única à medida que construímos a coss.com. Ao contrário dos primeiros dias da Cal.com, em que precisávamos de avançar depressa para encontrar product-market fit e construir um negócio sustentável, a coss.com começa numa posição de força.
-
Não precisamos de apressar a coss.com.
- A estabilidade da Cal.com significa que nos podemos permitir construir a coss.com da forma certa 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. Este é um ponto de partida fundamentalmente diferente.
-
A "lentidão" é um investimento, não um custo.
- Sim, seguir estes padrões pode parecer mais lento no início e até frustrante para alguns engenheiros. Escrever DTOs demora mais tempo do que passar tipos da base de dados diretamente para o frontend. Criar abstrações corretas e injeção de dependências requer mais design à partida. Manter uma cobertura de testes de 80%+ para novo código exige disciplina. Mas esta aparente lentidão é temporária, e o retorno é exponencial.
Considera os retornos compostos...
-
Código arquitetado corretamente desde o início não precisa de grandes refactors mais tarde
-
Uma elevada cobertura de testes evita bugs que, de outra forma, consumiriam semanas de debugging e hotfixes (vê 2023 até meados de 2024)
-
Abstrações corretas tornam a adição de novas funcionalidades dramaticamente mais rápida ao longo do tempo
-
Limites claros e DTOs evitam a erosão arquitetónica que, no fim, exige reescritas completas
A Cal.com trajetória mostra o que acontece quando otimizas para velocidade imediata. Alta velocidade inicial que se degrada gradualmente à medida que a dívida técnica se acumula, atalhos arquitetónicos criam estrangulamentos e se passa mais tempo a corrigir problemas do que a construir funcionalidades (vê o gráfico anterior, onde estávamos a gastar 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 ligeiramente mais lenta enquanto se estabelecem padrões corretos, seguida de uma aceleração exponencial à medida que esses padrões dão frutos e permitem um 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 menores raramente se concretizam. Em vez disso, acumulam-se como dívida técnica que nos pesa meses ou anos mais tarde. Se um pequeno refactor puder ser feito agora, faze-o agora. Os follow-ups devem ficar reservados para mudanças substanciais que realmente justifiquem PRs separados ou para casos excecionais e urgentes.
2. Padrões elevados na code review
-
Não deixes passar PRs com muitos nits só para evitar seres "a pessoa má".
- É precisamente assim que as codebases se tornam desleixadas ao longo do tempo. A code review não é sobre ser simpático. É sobre manter os padrões de qualidade que a nossa infraestrutura exige. Cada nitpick conta. Cada violação de padrão conta. Trata disso antes da merge, não depois.
3. Puxa pelos outros para fazerem a coisa certa
-
Responsabilizamo-nos mutuamente pela qualidade
- Cortar caminho pode parecer mais rápido no momento, mas cria problemas que abrandam toda a gente mais tarde. Quando vires um colega prestes a fazer merge de um PR com problemas óbvios, fala. Quando alguém sugerir um remendo rápido em vez da solução correta, contrapõe. Quando te tentares a saltar testes ou a ignorar padrões arquitetónicos, espera que os teus colegas te desafiem.
-
Isto não é sobre ser difícil ou abrandar pessoas
- É sobre responsabilidade coletiva pela nossa codebase e pela nossa reputação. Cada atalho que uma pessoa toma torna-se um problema de todos. Cada canto cortado hoje significa mais sessões de debugging, mais hotfixes e clientes mais frustrados amanhã.
-
Torna normal questionar decisões más, com respeito
- Se alguém disser "vamos só fazer hard-code disto por agora", a resposta esperada deve ser "o que seria preciso para fazer isto da forma certa à primeira?" Se alguém quiser fazer commit de código não testado, a equipa deve fazer resistência. Se alguém sugerir copiar e colar em vez de criar uma abstração correta, chama a atenção com respeito.
-
Estamos a construir algo que quase nunca pode falhar
- Esse nível de fiabilidade não acontece por acaso. Acontece quando cada engenheiro se sente responsável pela qualidade, não apenas pelo próprio código, mas pelo sistema inteiro. Temos sucesso como equipa ou falhamos como equipa.
4. Procura a simplicidade
-
Prioriza clareza em vez de esperteza
- O objetivo é código fácil de ler e de compreender rapidamente, não complexidade elegante. Sistemas simples reduzem a carga cognitiva de cada engenheiro.
-
Faz-te as perguntas certas
-
Estou realmente a resolver o problema em mãos?
-
Estou a pensar demasiado em possíveis casos de uso futuros?
-
Considerei pelo menos 1 outra alternativa para resolver isto? Como se compara?
-
-
Simples não quer dizer sem funcionalidades
- Só porque o nosso objetivo é criar sistemas simples, isso não significa que devam parecer anémicos e sem funcionalidade óbvia.
5. Automatiza tudo
-
Aproveita a IA
-
Gera 80% do boilerplate e do código não crítico usando IA, permitindo-nos focar-nos apenas na lógica de negócio complexa e nas arquiteturas críticas. As equipas que adotam workflows modernos assistidos por IA dependem cada vez mais de ferramentas como os best MCP servers para melhorar a produtividade dos developers e a orquestração.
-
Cria alertas sem ruído e tratamento inteligente de erros.
-
O testing manual é cada vez mais coisa do passado. A IA pode construir rapidamente e de forma inteligente mega suites de testes para nós.
-
-
O nosso CI é o boss final
-
Tudo neste documento de padrões é verificado antes de o código ser merged nos PRs
-
Nada de surpresas chega a main
-
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 padrões e princípios seguintes vão ser aplicados com rigor nas reviews de PR e através de linting.
Vertical Slice Architecture: packages/features
A nossa codebase 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 dentro dele representa uma vertical slice completa da aplicação, orientada pelo domínio que 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 esse 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
-
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 frontend relacionados com esta feature (quando aplicável)
-
Testes: Testes unitários, de integração e e2e para esta feature
Porque é que as vertical slices importam
A arquitetura tradicional em camadas organiza-se 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
Isto cria vários problemas:
-
Mudanças numa feature exigem tocar em ficheiros espalhados por vários diretórios
-
É difícil perceber o que faz uma funcionalidade porque o código dela está fragmentado
-
As equipas pisam os calcanhares umas das outras ao trabalhar em funcionalidades diferentes
-
Não consegues extrair ou descontinuar facilmente uma funcionalidade
A arquitetura em 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
Isto resolve estes problemas:
-
Tudo o que está relacionado com disponibilidade fica em
packages/features/availability -
Consegues entender toda a funcionalidade de disponibilidade explorando um único diretório
-
As equipas podem trabalhar em funcionalidades diferentes sem conflitos (se a equipa de engenharia da Cal.com crescer, mas muito certamente em coss.com teremos equipas a assumir grandes pacotes)
-
As funcionalidades estão fracamente acopladas e podem evoluir de forma independente
Diretrizes para a Organização de Funcionalidades
Em teoria, cada funcionalidade é implementável de forma independente. Embora possamos não as lançar separadamente na prática, organizar desta forma obriga-nos a manter as dependências claras e o acoplamento mínimo. Esta é a premissa e o sucesso dos microserviços, embora ainda não estejamos a implementar microserviços.
As funcionalidades comunicam através de interfaces bem definidas. Se bookings precisa de dados de disponibilidade, importa de @calcom/features/availability através de interfaces exportadas, e não entrando nos detalhes internos da implementação.
O código partilhado vive nos sítios adequados:
-
Utilitários agnósticos do domínio e preocupações transversais (autenticação, logging):
packages/lib -
Primitivas de UI partilhadas:
packages/ui(e em breve coss.com ui)
Os limites do domínio são impostos automaticamente. Vamos criar linting que impeça entrar nos internos das funcionalidades onde não deves ter acesso. Se packages/features/bookings tentar importar de packages/features/availability/services/internal, o linter bloqueia. Todas as dependências entre funcionalidades têm de passar pela API pública da funcionalidade.


As novas funcionalidades começam como fatias verticais. Quando construíres algo novo, cria uma nova pasta em packages/features com a fatia vertical completa. Isto deixa claro o que estás a construir e mantém tudo organizado desde o primeiro dia.
Benefícios
-
Facilidade de descoberta
- A procura da lógica de booking? Está tudo em
packages/features/bookings. Não precisas de andar à procura por controllers, services, repositories e utilities espalhados pela base de código.
- A procura da lógica de booking? Está tudo em
-
Testes mais fáceis
- Testa a funcionalidade inteira como uma unidade. Tens todas as peças num só sítio, o que torna os testes de integração naturais e diretos.
-
Dependências mais claras
- Quando vês
import { getAvailability } from '@calcom/features/availability', sabes exatamente de que funcionalidade dependes. Quando as dependências ficam demasiado complexas, isso torna-se óbvio e pode ser resolvido.
- Quando vês
Padrão de Repositório e Injeção de Dependência
As escolhas tecnológicas não devem infiltrar-se pela aplicação. O problema do Prisma ilustra isto na perfeição. Neste momento temos referências a Prisma espalhadas por centenas de ficheiros. Isto cria um acoplamento enorme e torna mudanças tecnológicas proibitivamente caras. Estamos a sentir agora essa dor ao atualizar o Prisma para v6.16. Algo que devia ter sido apenas um refactor localizado atrás de repositórios protegidos tornou-se numa busca sinuosa, quase interminável, de problemas em várias apps.
O padrão daqui para a frente:
-
Todo o acesso à base de dados tem de passar por classes Repository. Já temos uma boa vantagem inicial nisto.
-
Os repositories são o único código que conhece o Prisma (ou qualquer outro ORM). Não deve haver lógica neles.
-
Os repositories são injetados através de containers de Dependency Injection
-
Se algum dia trocarmos de Prisma para Drizzle ou outro ORM, as únicas alterações necessárias são:
-
Implementações de repositories
-
Configuração do container de DI para novos repositories
-
Mais nada na base de código deve preocupar-se com isso ou mudar
-
Isto não é teórico. É assim que construímos sistemas manteníveis.
Objectos de Transferência de Dados (DTOs)
Os tipos da base de dados não devem vazar para o frontend. Isto tornou-se um atalho popular na nossa stack tecnológica, mas é um código com cheiro que cria vários problemas.
-
Acoplamento tecnológico (tipos Prisma acabam em componentes React)
-
Riscos de segurança (vazamento acidental de campos sensíveis)
-
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 para DTOs através de Zod, mesmo para uma resposta de API, para garantir que todos os dados são validados antes de serem enviados ao utilizador. Mais vale falhar do que devolver algo errado.
O padrão daqui para a frente:
Cria DTOs explícitos em cada fronteira arquitetónica.
-
Camada de dados → Camada de aplicação → API: Transforma os modelos da base de dados em DTOs da camada de aplicação e depois transforma os DTOs da aplicação em DTOs específicos da API
-
API → Camada de aplicação → Camada de dados: Transforma os DTOs da API através da camada de aplicação e em DTOs específicos dos dados
Sim, isto exige mais código. Sim, vale a pena. Fronteiras explícitas evitam a erosão arquitetónica que cria pesadelos de manutenção a longo prazo.
Padrões de Design Orientado ao Domínio
Os padrões seguintes têm de ser usados de forma correta e consistente:
-
Application Services
- Orquestram casos de uso, coordenam entre domain services e repositories
-
Domain Services
- Contêm lógica de negócio que naturalmente não pertence a uma única entidade
-
Repositories
- Abstraem o acesso a dados, isolam escolhas tecnológicas
-
Dependency Injection
- Permite acoplamento fraco, facilita testes, isola responsabilidades
-
Caching Proxies
-
Envolvem 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 do domínio
Consistência da Base de Código
As nossas bases de código devem parecer que foram escritas por uma só pessoa. Este nível de consistência exige adesão rigorosa aos padrões estabelecidos, regras de linting abrangentes que imponham os standards arquitetónicos, code reviews que rejeitem violações de padrões + a ajuda de revisores de código com IA.
Move as condicionais para o ponto de entrada da aplicação
Os ifs pertencem ao ponto de entrada, não espalhados pelos teus services. Este é um dos princípios arquitetónicos mais importantes para manter código limpo e focado, que não desliza para uma complexidade impossível de manter.
Eis 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. Depois chega um novo requisito de produto e alguém acrescenta um if. Uns anos e mais alguns requisitos depois, esse service está cheio de verificações condicionais para cenários diferentes. O service tornou-se:
-
Complicado e difícil de ler
-
Difícil de entender e raciocinar sobre ele
-
Mais suscetível a bugs
-
A violar a responsabilidade única (a tratar demasiados casos diferentes)
-
Quase impossível de testar exaustivamente
O service ultrapassou os seus limites em termos de responsabilidades e lógica.
Uma solução: padrão Factory com services especializados
Usa o padrão factory para tomar decisões no ponto de entrada e depois delega em services 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 depois devolve 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 trata então da sua lógica específica sem precisar de verificar "sou uma org 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],
});
}
// ... more team-specific logic
}
Porque é que isto importa
-
Os services mantêm-se focados
- Cada service tem uma responsabilidade e não precisa de saber sobre outros contextos. O
OrganizationBillingPortalServicenão contém ifs a verificarif (isTeam)ouif (isUser). Só sabe como lidar com organizações.
- Cada service tem uma responsabilidade e não precisa de saber sobre outros contextos. O
-
As alterações ficam isoladas
- Quando precisares de modificar a lógica de faturação da organização, só tocas em
OrganizationBillingPortalService. Não corres o risco de partir a faturação da equipa ou do utilizador. Não precisas de seguir condicionais aninhadas para perceber que caminho o teu código segue.
- Quando precisares de modificar a lógica de faturação da organização, só tocas em
-
Os testes são diretos
- Testa cada serviço de forma independente com os 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 precisas de adicionar faturação enterprise com regras diferentes, crias
EnterpriseBillingPortalService. A factory ganha mais um condicional, mas os serviços existentes mantêm-se intocados e focados.
- por exemplo, quando precisas de adicionar faturação enterprise com regras diferentes, crias
Como conseguir
-
Empurra os condicionais para cima, para controllers, factories ou lógica de routing. Deixa que estes pontos de entrada tomem decisões sobre que serviço usar.
-
Mantém os serviços puros e focados numa única responsabilidade. Se um serviço precisa de verificar "que tipo sou eu?", provavelmente precisas de vários serviços.
-
Prefere polimorfismo em vez de condicionais
- As interfaces definem o contrato. As implementações concretas fornecem os detalhes.
-
Atenção à acumulação de if statements
- Durante a revisão de código, se vires um serviço a ganhar condicionais para diferentes cenários, isso é sinal para refatorar para serviços especializados.
Design da API: Controllers leves e abstração HTTP
-
Os controllers são camadas leves que tratam apenas de questões HTTP.
- Recebem pedidos, processam-nos e mapeiam dados para DTOs que são passados para a lógica central da aplicação. Daqui para a frente, nenhuma lógica da aplicação ou central deve ser vista em API routes ou handlers tRPC.
-
Temos de separar a tecnologia HTTP da nossa aplicação.
- A forma como transferimos dados entre cliente e servidor (seja REST, tRPC, etc.) não deve influenciar o funcionamento da nossa aplicação central. HTTP é um mecanismo de entrega, não um motor arquitetural.
Responsabilidades do controller (e APENAS estas):
-
Receber e validar pedidos de entrada
-
Extrair dados de parâmetros do pedido, corpo, headers
-
Transformar dados do pedido em DTOs
-
Chamar os serviços de aplicação adequados com esses DTOs
-
Transformar as respostas dos serviços da aplicação em response DTOs
-
Devolver respostas HTTP com códigos de estado corretos
Os controllers NÃO devem:
-
Conter lógica de negócio ou regras de domínio
-
Aceder diretamente a bases de dados ou serviços externos
-
Fazer transformações de dados complexas 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 leve:
// 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 da API e mudanças disruptivas
Sem mudanças disruptivas. Isto é crítico. Assim que um endpoint da API é público, tem de continuar estável. Mudanças disruptivas destroem a confiança dos developers e criam pesadelos de integração para os nossos utilizadores.
Estratégias para evitar mudanças disruptivas:
-
Adiciona sempre novos campos como opcionais
-
Usa versionamento da API quando tens mesmo de alterar o comportamento existente
-
Descontinua endpoints antigos de forma graciosa com caminhos de migração claros
-
Mantém compatibilidade retroativa durante, pelo menos, duas versões principais
Quando tens mesmo de fazer mudanças disruptivas:
-
Cria uma nova versão da API usando o versionamento baseado em data em API v2 (talvez venhamos a analisar também o versionamento com nomes que a Stripe introduziu recentemente)
-
Executa ambas as versões em simultâneo durante a transição (já fazemos isto em API v2)
-
Fornece ferramentas automáticas de migração quando possível
-
Dá aos utilizadores tempo suficiente para migrarem (mínimo de 6 meses para APIs públicas)
-
Documenta exatamente o que mudou e porquê
Performance e complexidade algorítmica
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 enterprise. A performance não é algo que otimizamos mais tarde. É algo que construímos corretamente desde o início.
Pensa na escala desde o primeiro dia
Ao construir funcionalidades, pergunta 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 aninhadas em arrays (
.mapdentro de.map,.forEachdentro de.forEach) -
Métodos de arrays como
.some,.findou.filterdentro de loops ou callbacks -
Verificar cada item contra todos os outros sem otimização
-
Filtros em cadeia ou mapeamento aninhado sobre listas grandes
Exemplo real: Para 100 slots disponíveis e 50 períodos ocupados, um algoritmo O(n²) faz 5.000 verificações. Se escalares para 500 slots e 200 períodos ocupados, estás a fazer 100.000 operações. Isso é um aumento de 20x na carga computacional para apenas um aumento de 5x nos dados.
Escolhe as estruturas de dados e algoritmos certos
A maioria dos problemas de performance resolve-se escolhendo melhores estruturas de dados e algoritmos:
-
Ordenação + saída antecipada: Ordena os teus dados uma vez e depois sai dos loops quando souberes que os itens restantes não vão corresponder
-
Pesquisa binária: Usa pesquisa binária para pesquisas em arrays ordenados em vez de varrimentos lineares
-
Técnicas de dois ponteiros: Para fundir ou intersectar sequências ordenadas, percorre ambas com ponteiros em vez de loops aninhados
-
Hash maps/sets: Usa objects ou Sets para pesquisas O(1) em vez de
.findou.includesem arrays -
Árvores de intervalos: Para scheduling, disponibilidade e consultas de intervalos, usa estruturas de árvore adequadas em vez de comparação brute-force
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 automáticas de performance
Vamos implementar várias camadas de defesa contra regressões de performance:
Regras de linting que assinalam:
-
Funções com loops aninhados ou métodos de arrays aninhados
-
Chamadas múltiplas e aninhadas a
.some,.findou.filter -
Recursão sem memoization
-
Anti-padrões conhecidos para o nosso domínio (scheduling, verificações de disponibilidade, etc.)
Benchmarks de performance em CI que:
-
Executam algoritmos críticos com dados realistas e em grande escala
-
Comparam os tempos de execução com a linha base em cada PR
-
Bloqueiam merges que introduzem regressões de performance
-
Testam com dados à escala enterprise (milhares de utilizadores, dezenas de milhares de registos)
Monitorização em produção que:
-
Acompanha o tempo de execução dos caminhos críticos
-
Alerta quando os algoritmos ficam mais lentos à medida que os dados crescem
-
Deteta regressões antes de os utilizadores darem por elas
-
Fornece dados reais de performance para orientar otimizações
Performance é uma funcionalidade
A performance não é opcional. Não é algo que "deixamos para depois." Para clientes empresariais que marcam reuniões entre equipas grandes, respostas lentas significam perda de produtividade e utilizadores frustrados (a nossa experiência com alguns clientes empresariais maiores pode comprovar isso).
Todo o engenheiro deve:
-
Fazer perfil do teu 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 registos de teste). Já temos scripts de seed prontos. Provavelmente precisamos de os estender.
-
Escolher antecipadamente algoritmos e estruturas de dados eficientes
-
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-hard do agendamento
Os problemas de agendamento são fundamentalmente NP-hard. Isto 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 ótimos de agendamento tem complexidade temporal exponencial no pior caso, o que torna a escolha do algoritmo absolutamente crítica.
Implicações no mundo real:
-
Encontrar a hora ideal para uma reunião de 10 pessoas em 3 fusos horários, com restrições individuais de disponibilidade, é computacionalmente caro
-
Adicionar deteção de conflitos, intervalos e uma série de outras opções amplifica o problema
-
Más escolhas de algoritmo que funcionam bem para equipas pequenas tornam-se completamente inutilizáveis para organizações grandes
-
O que leva milissegundos para 5 utilizadores pode levar muitos segundos para organizações
Estratégias para gerir a complexidade NP-hard:
-
Usar algoritmos de aproximação que encontram soluções "boas o suficiente" rapidamente, em vez de soluções perfeitas lentamente
-
Implementar caching agressivo de horários e disponibilidade calculados
-
Pré-calcular cenários comuns durante horas de menor tráfego
-
Dividir grandes problemas de agendamento em blocos mais pequenos e mais fáceis de gerir
-
Definir limites de timeout razoáveis e recorrer a algoritmos mais simples quando necessário
É por isso que o desempenho não é apenas um extra simpático no software de agendamento. É a base que determina se o teu sistema consegue escalar para necessidades empresariais ou colapsa perante padrões de utilização do mundo real.
Requisitos de cobertura de código
-
Acompanhamento global da cobertura
- Acompanhamos a cobertura global da base de código como uma métrica-chave que melhora ao longo do tempo. Isto dá-nos visibilidade sobre a maturidade dos nossos testes e ajuda a identificar áreas que precisam de atenção. A percentagem global de cobertura é apresentada de forma destacada nos nossos dashboards.
-
Cobertura de 80%+ para novo código
- Cada PR tem de ter uma cobertura de testes de perto de 80%+ para o código que introduz ou modifica. Isto é aplicado automaticamente no nosso pipeline de CI. Se adicionares 50 linhas de código novo, essas 50 linhas têm de estar cobertas por testes. Se modificares uma função existente, as tuas alterações têm de ser testadas. Isto é cobertura total de testes. A cobertura de testes unitários tem de estar perto de 100%, especialmente com a capacidade de usar IA para ajudar a gerar estes testes.
A responder ao argumento de que "a cobertura não conta a história toda": Sim, sabemos que a cobertura não garante testes perfeitos. Sabemos que podes escrever testes sem significado que atingem todas as linhas mas não testam nada de relevante. Sabemos que a cobertura é apenas uma métrica entre muitas. Mas, claro que é melhor apontar para uma percentagem alta do que não teres ideia nenhuma de onde estás.
Medir o sucesso
-
"Velocidade" (roubado do Scrum, embora não vamos usar Scrum)
- Crescimento contínuo nas estatísticas mensais (funcionalidades, melhorias, refactors)
-
Qualidade
- Reduzir o esforço de PR gasto em correções dos atuais 35% para 20% ou menos até ao fim de 2026 (calculado com base em alterações e adições/remoções de ficheiros)
-
Saúde arquitetural
- Métricas sobre adesão a padrões, acoplamento tecnológico, violações de fronteiras
-
Eficiência de revisão
- PRs mais pequenos, revisões mais rápidas, menos rondas de feedback
-
Tempo de atividade da aplicação e da API
- Quão perto estamos de 99.99%?
