Blog

Engineering en 2026 et au-delà

Nous construisons une infrastructure qui doit presque jamais tomber en panne. Pour y parvenir, nous avançons vite tout en livrant des logiciels d’une qualité remarquable, sans raccourcis ni compromis. Ce document présente les normes d’ingénierie qui nous guideront en 2026 et au-delà.

Keith WilliamsOct 14, 2025

Structure de l’équipe

Notre organisation d’ingénierie se compose de cinq équipes principales, chacune avec des responsabilités distinctes :

  • Foundation Team : Se concentre sur l’établissement et le maintien des standards de code et des modèles architecturaux. Cette équipe travaille en collaboration avec les autres équipes pour établir les meilleures pratiques dans toute l’organisation.

  • Consumer, Enterprise, and Platform Teams : Des équipes axées produit qui livrent rapidement des fonctionnalités tout en maintenant les standards de qualité définis dans ce document. Ces équipes démontrent que vitesse et qualité ne s’excluent pas mutuellement.

  • Community Team : Responsable d’examiner rapidement les PRs de la communauté open source, de fournir des retours et d’accompagner ce travail jusqu’à sa fusion. Cette équipe veille à ce que nos contributeurs open source vivent une excellente expérience et que leurs contributions respectent nos standards de qualité. Le maintien d’une forte culture open-source continue de façonner la manière dont Cal.com aborde la collaboration en ingénierie et le développement produit.

Nos résultats à ce jour

Les chiffres parlent d’eux-mêmes. Au cours de l’année et demie écoulée, nous avons transformé en profondeur notre manière de construire des logiciels :

Screenshot 2025-10-01 at 9.40.36 PM.png

Nous avons à peu près doublé notre productivité en ingénierie tout en améliorant simultanément la qualité. Plus impressionnant encore, c’est le changement dans ce que nous construisons :

Screenshot 2025-10-01 at 9.41.51 PM.png

Nous avons réussi à réallouer environ 20 % des efforts d’ingénierie loin des correctifs et vers les fonctionnalités, les améliorations de performance, les refactorings et les tâches de maintenance. Ce changement montre qu’investir dans la qualité et l’architecture ne vous ralentit pas. Cela vous accélère.

La base de Cal.com permet l’excellence pour coss.com

  • Cal.com est une entreprise stable et rentable que nous continuerons à développer.

    • Ce succès nous donne un avantage unique alors que nous construisons coss.com. Contrairement aux débuts de Cal.com, où nous devions avancer vite pour atteindre l’adéquation produit-marché et bâtir une entreprise durable, coss.com part d’une position de force.
  • Nous n’avons pas besoin de nous précipiter pour coss.com.

    • La stabilité de Cal.com nous permet de construire coss.com comme il faut dès le premier jour. Nous avons le luxe de mettre en place ces standards d’ingénierie sans la pression de demandes immédiates du marché ni de contraintes de financement. C’est une position de départ fondamentalement différente.
  • La « lenteur » est un investissement, pas un coût.

    • Oui, suivre ces standards peut sembler plus lent au début et peut même frustrer certains ingénieurs. Écrire des DTOs prend plus de temps que de transmettre directement les types de base de données au frontend. Créer des abstractions propres et de l’injection de dépendances demande davantage de conception en amont. Maintenir une couverture de test de 80 % et plus pour le nouveau code exige de la discipline. Mais cette lenteur apparente est temporaire, et le rendement est exponentiel.

Considérez les effets composés...

  • Un code architecturé correctement dès le départ n’a pas besoin de refactorings massifs plus tard

  • Une couverture de test élevée évite des bugs qui consommeraient autrement des semaines de débogage et de hotfixes (voir 2023 à mi-2024)

  • Des abstractions propres accélèrent considérablement l’ajout de nouvelles fonctionnalités au fil du temps

  • Des frontières claires et des DTOs empêchent l’érosion architecturale qui finit par exiger des réécritures complètes

La Cal.com trajectoire montre ce qui se passe quand vous optimisez pour une vitesse immédiate. Une vitesse initiale élevée qui se dégrade progressivement à mesure que la dette technique s’accumule, que des raccourcis architecturaux créent des goulots d’étranglement, et que de plus en plus de temps est consacré à corriger des problèmes plutôt qu’à construire des fonctionnalités (voir le graphique précédent où nous consacrions 55 à 60 % des efforts d’ingénierie aux correctifs).

La trajectoire de coss.com adoptera la puissance d’une construction correcte dès le premier jour. Une vélocité initiale légèrement plus faible pendant la mise en place de modèles appropriés, suivie d’une accélération exponentielle à mesure que ces modèles portent leurs fruits et permettent un développement plus rapide avec davantage de confiance.

Principes fondamentaux

1. Aucune qualité différée

  • Nous réduirons au minimum le « je le ferai dans une PR de suivi » pour les petits refactorings.

    • Les PRs de suivi pour des améliorations mineures se matérialisent rarement. À la place, elles s’accumulent en dette technique qui nous pèse des mois ou des années plus tard. Si un petit refactoring peut être fait maintenant, faites-le maintenant. Les suivis doivent être réservés aux changements substantiels qui méritent réellement des PRs séparées ou à des cas exceptionnels et urgents.

2. Des standards élevés en revue de code

  • Ne laissez pas passer des PRs avec beaucoup de petites remarques juste pour éviter d’être « la personne méchante ».

    • C’est précisément ainsi que les bases de code deviennent négligées au fil du temps. La revue de code n’est pas une question de gentillesse. Il s’agit de maintenir les standards de qualité qu’exige notre infrastructure. Chaque remarque compte. Chaque violation de pattern compte. Traitez-les avant la fusion, pas après.

3. Poussez-vous mutuellement à faire ce qu’il faut

  • Nous nous tenons mutuellement responsables de la qualité

    • Couper les coins peut sembler plus rapide sur le moment, mais cela crée des problèmes qui ralentissent tout le monde plus tard. Quand vous voyez un coéquipier sur le point de fusionner une PR avec des problèmes évidents, dites-le. Quand quelqu’un propose un bricolage rapide au lieu de la bonne solution, opposez-vous-y. Quand vous êtes tenté de sauter des tests ou d’ignorer des modèles architecturaux, attendez-vous à ce que vos coéquipiers vous challengent.
  • Il ne s’agit pas d’être difficile ni de ralentir les gens

    • Il s’agit de la responsabilité collective de notre base de code et de notre réputation. Chaque raccourci pris par une personne devient le problème de tout le monde. Chaque coin coupé aujourd’hui signifie demain plus de sessions de débogage, plus de hotfixes et plus de clients frustrés.
  • Faites en sorte qu’il devienne normal de contester les mauvaises décisions, avec respect

    • Si quelqu’un dit « mettons ça en dur pour l’instant », la réponse attendue devrait être « qu’est-ce qu’il faudrait pour le faire correctement du premier coup ? ». Si quelqu’un veut commettre du code non testé, l’équipe doit s’y opposer. Si quelqu’un suggère de copier-coller au lieu de créer une abstraction propre, signalez-le avec respect.
  • Nous construisons quelque chose qui doit presque jamais échouer

    • Ce niveau de fiabilité n’arrive pas par hasard. Il arrive quand chaque ingénieur se sent responsable de la qualité, pas seulement de son propre code, mais de tout le système. Nous réussissons en équipe ou nous échouons en équipe.

4. Visez la simplicité

  • Privilégiez la clarté à l’ingéniosité

    • L’objectif est d’avoir du code facile à lire et à comprendre rapidement, pas une complexité élégante. Des systèmes simples réduisent la charge cognitive de chaque ingénieur.
  • Posez-vous les bonnes questions

    • Est-ce que je résous vraiment le problème à traiter ?

    • Est-ce que je réfléchis trop à d’éventuels cas d’usage futurs ?

    • Ai-je envisagé au moins une autre alternative pour résoudre cela ? Comment se compare-t-elle ?

  • Simple ne veut pas dire dépourvu de fonctionnalités

    • Ce n’est pas parce que notre objectif est de créer des systèmes simples qu’ils doivent paraître anémiques et manquer de fonctionnalités évidentes.

5. Automatisez tout

  • Tirez parti de l’IA

    • Générez 80 % du code répétitif et non critique à l’aide de l’IA, ce qui nous permet de nous concentrer uniquement sur la logique métier complexe et les architectures critiques. Les équipes qui adoptent des workflows modernes assistés par l’IA s’appuient de plus en plus sur des outils comme les meilleurs serveurs MCP pour améliorer la productivité des développeurs et l’orchestration.

    • Mettez en place une alerte sans bruit et une gestion intelligente des erreurs.

    • Les tests manuels appartiennent de plus en plus au passé. L’IA peut rapidement et intelligemment construire pour nous d’immenses suites de tests.

  • Notre CI est le boss final

    • Tout ce document de normes est vérifié avant que le code soit fusionné dans les PRs

    • Aucune surprise n’arrive dans main

    • Les vérifications sont rapides et utiles

Standards architecturaux

Nous passons à un modèle architectural strict basé sur Vertical Slice Architecture et Domain-Driven Design (DDD). Les modèles et principes suivants seront appliqués avec rigueur lors des revues de PRs et via le linting.

Vertical Slice Architecture : packages/features

Notre base de code est organisée par domaine, et non par couche technique. Le répertoire packages/features est le cœur de cette approche architecturale. Chaque dossier qu’il contient représente une tranche verticale complète de l’application, guidée par le domaine qu’il touche.

Structure :

packages/features/  
  availability/  
  bookings/  
  ooo/  
  payments/  
  routing/  
  scheduling/  
  ...

Chaque dossier de fonctionnalité est une tranche verticale autonome qui comprend tout ce qui est nécessaire pour ce domaine :

  • Logique métier : Les règles métier essentielles et les entités propres à cette fonctionnalité

  • Services applicatifs : L’orchestration des cas d’usage pour ce domaine

  • Repositories : L’accès aux données spécifique aux besoins de cette fonctionnalité

  • DTOs : Des objets de transfert de données pour franchir les frontières

  • Composants UI : Les composants frontend liés à cette fonctionnalité (le cas échéant)

  • Tests : Des tests unitaires, d’intégration et e2e pour cette fonctionnalité

Pourquoi les tranches verticales sont importantes

L’architecture en couches traditionnelle organise par préoccupations techniques :

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

Cela crée plusieurs problèmes :

  • Les changements apportés à une fonctionnalité nécessitent de toucher des fichiers dispersés dans plusieurs répertoires

  • Il est difficile de comprendre ce que fait une fonctionnalité parce que son code est fragmenté

  • Les équipes se gênent mutuellement lorsqu’elles travaillent sur différentes fonctionnalités

  • Vous ne pouvez pas facilement extraire ou rendre obsolète une fonctionnalité

L’architecture en tranche verticale organise par domaine :

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

Cela résout ces problèmes :

  • Tout ce qui est lié à la disponibilité se trouve dans packages/features/availability

  • Vous pouvez comprendre toute la fonctionnalité de disponibilité en explorant un seul répertoire

  • Les équipes peuvent travailler sur différentes fonctionnalités sans conflits (si l’équipe d’ingénierie de Cal.com grandit, mais très certainement chez coss.com nous ferons en sorte que des équipes prennent en charge des packages majeurs)

  • Les fonctionnalités sont faiblement couplées et peuvent évoluer indépendamment

Directives pour l’organisation des fonctionnalités

En théorie, chaque fonctionnalité est déployable indépendamment. Même si nous ne les déployons pas forcément séparément, les organiser ainsi nous oblige à garder des dépendances claires et un couplage minimal. C’est le principe et la réussite des microservices, même si nous ne déploierons pas encore de microservices.

Les fonctionnalités communiquent via des interfaces bien définies. Si bookings a besoin de données de disponibilité, il les importe depuis @calcom/features/availability via les interfaces exportées, et non en allant fouiller dans les détails d’implémentation internes.

Le code partagé vit aux bons endroits :

  • Les utilitaires agnostiques du domaine et les préoccupations transversales (auth, logging) : packages/lib

  • Les primitives d’UI partagées : packages/ui (et bientôt coss.com ui)

Les frontières de domaine sont appliquées automatiquement. Nous allons construire un linting qui empêche d’aller dans les entrailles des fonctionnalités là où vous ne devriez pas être autorisé. Si packages/features/bookings essaie d’importer depuis packages/features/availability/services/internal, le linter le bloquera. Toutes les dépendances entre fonctionnalités doivent passer par l’API publique de la fonctionnalité.

good_architecture_diagram.pngbad_architecture_diagram.png

Les nouvelles fonctionnalités commencent comme des tranches verticales. Quand vous créez quelque chose de nouveau, créez un nouveau dossier dans packages/features avec la tranche verticale complète. Cela clarifie ce que vous construisez et garde tout organisé dès le premier jour.

Avantages

  • Facilité de découverte

    • Vous cherchez la logique de réservation ? Tout est dans packages/features/bookings. Pas besoin de fouiller dans des controllers, services, repositories et utilitaires éparpillés dans la base de code.
  • Tests plus faciles

    • Testez toute la fonctionnalité comme une unité. Vous avez toutes les pièces au même endroit, ce qui rend les tests d’intégration naturels et simples.
  • Dépendances plus claires

    • Quand vous voyez import { getAvailability } from '@calcom/features/availability', vous savez exactement de quelle fonctionnalité vous dépendez. Quand les dépendances deviennent trop complexes, c’est évident et cela peut être corrigé.

Modèle Repository et injection de dépendances

Les choix technologiques ne doivent pas se propager dans toute l’application. Le problème Prisma l’illustre parfaitement. Nous avons actuellement des références à Prisma disséminées dans des centaines de fichiers. Cela crée un couplage massif et rend les changements technologiques prohibitivement coûteux. Nous ressentons maintenant cette douleur en mettant à jour Prisma vers v6.16. Ce qui aurait dû être une simple refactorisation localisée derrière des repositories protégés s’est transformé en une chasse aux problèmes sinueuse et presque sans fin à travers plusieurs applications.

La norme à partir de maintenant :

  • Tous les accès à la base de données doivent passer par des classes Repository. Nous avons déjà une bonne longueur d’avance là-dessus.

  • Les repositories sont le seul code qui connaît Prisma (ou tout autre ORM). Aucun logique ne doit y figurer.

  • Les repositories sont injectés via des conteneurs Dependency Injection

  • Si nous passons un jour de Prisma à Drizzle ou à un autre ORM, les seuls changements nécessaires sont :

    • Les implémentations des repositories

    • Le câblage du conteneur DI pour les nouveaux repositories

    • Rien d’autre dans la base de code ne devrait s’en soucier ni changer

Ce n’est pas théorique. C’est ainsi que nous construisons des systèmes maintenables.

Data Transfer Objects (DTOs)

Les types de base de données ne doivent pas fuiter vers le frontend. C’est devenu un raccourci populaire dans notre stack technique, mais c’est une odeur de code qui crée plusieurs problèmes.

  • Couplage technologique (les types Prisma finissent dans des composants React)

  • Risques de sécurité (fuite accidentelle de champs sensibles)

  • Contrats fragiles entre le serveur et le client (c’est particulièrement problématique à mesure que nous construisons bien plus d’API)

  • Incapacité à faire évoluer le schéma de base de données indépendamment

  • Toutes les conversions DTO passent par Zod, même pour une réponse d’API, afin de garantir que toutes les données sont validées avant l’envoi à l’utilisateur. Mieux vaut échouer que renvoyer quelque chose de faux.

La norme à partir de maintenant :
Créez des DTO explicites à chaque frontière architecturale.

  1. Couche de données → Couche application → API : transformez les modèles de base de données en DTO de la couche application, puis transformez les DTO de la couche application en DTO spécifiques à l’API

  2. API → Couche application → Couche de données : transformez les DTO d’API via la couche application en DTO spécifiques aux données

Oui, cela demande plus de code. Oui, cela en vaut la peine. Des frontières explicites empêchent l’érosion architecturale qui crée des cauchemars de maintenance à long terme.

Patterns de conception pilotée par le domaine

Les patterns suivants doivent être utilisés correctement et de manière cohérente :

  • Application Services

    • Orchestrent les cas d’usage, coordonnent les domain services et les repositories
  • Domain Services

    • Contiennent la logique métier qui n’appartient pas naturellement à une seule entité
  • Repositories

    • Abstraient l’accès aux données, isolent les choix technologiques
  • Dependency Injection

    • Permet un faible couplage, facilite les tests, isole les responsabilités
  • Caching Proxies

    • Enveloppent des repositories ou des services pour ajouter de façon transparente un comportement de cache

    • Ce n’est bien sûr pas la seule manière de faire du cache, mais c’est un bon point de départ

  • Decorators

    • Ajoutent des préoccupations transversales (logging, metrics, etc.) sans polluer la logique métier

Cohérence de la base de code

Nos bases de code devraient donner l’impression qu’une seule personne les a écrites. Ce niveau de cohérence exige une adhésion stricte aux patterns établis, des règles de linting complètes qui imposent les standards architecturaux, des code reviews qui rejettent les violations de pattern + l’aide de relecteurs de code IA.

Déplacez les conditions vers le point d’entrée de l’application

Les if doivent se trouver au point d’entrée, pas dispersés dans vos services. C’est l’un des principes architecturaux les plus importants pour maintenir un code propre, focalisé, qui ne sombre pas dans une complexité ingérable.

Voici comment le code se dégrade avec le temps : Un service est écrit pour un objectif clair et précis. La logique est propre et ciblée. Puis une nouvelle exigence produit arrive, et quelqu’un ajoute un if. Quelques années et plusieurs autres exigences plus tard, ce service est constellé de vérifications conditionnelles pour différents scénarios. Le service est devenu :

  • Compliqué et difficile à lire

  • Difficile à comprendre et à raisonner

  • Plus susceptible de contenir des bugs

  • En violation du principe de responsabilité unique (il gère trop de cas différents)

  • Presque impossible à tester complètement

Le service a dépassé ses limites en termes de responsabilités et de logique.

Une solution : le pattern Factory avec des services spécialisés
Utilisez le pattern factory pour prendre les décisions au point d’entrée, puis déléguez à des services spécialisés qui gèrent leur logique spécifique sans conditions.

Exemple dans notre base de code :
BillingPortalServiceFactory détermine si la facturation concerne une organisation, une équipe ou un utilisateur individuel, puis renvoie le service approprié :

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();  
  }
}

Chaque service gère ensuite sa logique spécifique sans avoir besoin de vérifier « suis-je une org ou une team ? » :

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

Pourquoi c’est important

  • Les services restent focalisés

    • Chaque service a une seule responsabilité et n’a pas besoin de connaître les autres contextes. OrganizationBillingPortalService ne contient pas de if qui vérifient if (isTeam) ou if (isUser). Il sait seulement gérer les organisations.
  • Les changements sont isolés

    • Lorsque vous devez modifier la logique de facturation de l’organisation, vous ne touchez qu’à OrganizationBillingPortalService. Vous ne risquez pas de casser la facturation des équipes ou des utilisateurs. Vous n’avez pas besoin de remonter des conditions imbriquées pour comprendre quelle voie prend votre code.
  • Les tests sont simples

    • Testez chaque service indépendamment avec ses scénarios spécifiques. Pas besoin de tester toutes les combinaisons de conditions dans différents contextes.
  • Les nouvelles exigences ne polluent pas le code existant

    • par ex. lorsque vous devez ajouter une facturation Enterprise avec des règles différentes, vous créez EnterpriseBillingPortalService. La factory gagne une condition de plus, mais les services existants restent inchangés et ciblés.

Comment y parvenir

  • Remontez les conditions vers les contrôleurs, les factories ou la logique de routage. Laissez ces points d’entrée décider quel service utiliser.

  • Gardez des services purs et centrés sur une seule responsabilité. Si un service doit se demander « de quel type suis-je ? », il vous faut probablement plusieurs services.

  • Privilégiez le polymorphisme aux conditions

    • Les interfaces définissent le contrat. Les implémentations concrètes fournissent les détails.
  • Surveillez l’accumulation de if

    • Lors de la revue de code, si vous voyez qu’un service accumule des conditions pour différents scénarios, c’est le signe qu’il faut le refactoriser en services spécialisés.

Conception de l’API : contrôleurs minces et abstraction HTTP

  • Les contrôleurs sont des couches fines qui ne gèrent que les aspects HTTP.

    • Ils reçoivent les requêtes, les traitent et mappent les données vers des DTO transmis à la logique métier principale. À l’avenir, aucune logique applicative ou métier ne doit apparaître dans les routes API ou les handlers tRPC.
  • Nous devons détacher la technologie HTTP de notre application.

    • La manière dont nous transférons les données entre le client et le serveur (REST, tRPC, etc.) ne doit pas influencer le fonctionnement de notre application principale. HTTP est un mécanisme de livraison, pas un moteur d’architecture.

Responsabilités des contrôleurs (et SEULEMENT celles-ci) :

  • Recevoir et valider les requêtes entrantes

  • Extraire les données des paramètres de requête, du corps, des en-têtes

  • Transformer les données de la requête en DTO

  • Appeler les services applicatifs appropriés avec ces DTO

  • Transformer les réponses des services applicatifs en DTO de réponse

  • Renvoyer des réponses HTTP avec les codes de statut appropriés

Les contrôleurs ne doivent PAS :

  • Contenir de logique métier ni de règles de domaine

  • Accéder directement aux bases de données ou à des services externes

  • Effectuer des transformations de données ou des calculs complexes

  • Prendre des décisions sur ce que l’application doit faire

  • Connaître les détails d’implémentation du domaine

Exemple de modèle de contrôleur mince :

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

Versioning de l’API et changements cassants

Aucun changement cassant. C’est critique. Une fois qu’un endpoint API est public, il doit rester stable. Les changements cassants détruisent la confiance des développeurs et créent des cauchemars d’intégration pour nos utilisateurs.

Stratégies pour éviter les changements cassants :

  • Ajoutez toujours les nouveaux champs comme optionnels

  • Utilisez le versioning de l’API lorsque vous devez modifier un comportement existant

  • Dépréciez les anciens endpoints progressivement avec des chemins de migration clairs

  • Maintenez la compatibilité ascendante pendant au moins deux versions majeures

Quand vous devez faire des changements cassants :

  • Créez une nouvelle version de l’API en utilisant le versioning basé sur la date dans API v2 (peut-être que nous examinerons aussi le versioning par nom récemment introduit par Stripe)

  • Faites fonctionner les deux versions simultanément pendant la transition (nous le faisons déjà dans API v2)

  • Fournissez des outils de migration automatisés lorsque c’est possible

  • Laissez aux utilisateurs suffisamment de temps pour migrer (minimum 6 mois pour les API publiques)

  • Documentez précisément ce qui a changé et pourquoi

Performance et complexité algorithmique

Nous concevons pour de grandes organisations et de grandes équipes. Ce qui fonctionne correctement avec 10 utilisateurs ou 50 enregistrements peut s’effondrer sous le poids d’une échelle Enterprise. La performance n’est pas quelque chose que nous optimisons plus tard. C’est quelque chose que nous construisons correctement dès le départ.

Pensez à l’échelle dès le premier jour

Lorsque vous construisez des fonctionnalités, demandez-vous toujours : « Comment cela se comporte-t-il avec 1 000 utilisateurs ? 10 000 enregistrements ? 100 000 opérations ? » La différence entre des algorithmes O(n) et O(n²) peut être imperceptible en développement, mais catastrophique en production.

Modèles O(n²) courants à éviter :

  • Parcours imbriqués de tableaux (.map dans .map, .forEach dans .forEach)

  • Méthodes de tableau comme .some, .find ou .filter à l’intérieur de boucles ou de callbacks

  • Vérifier chaque élément par rapport à tous les autres sans optimisation

  • Filtres en chaîne ou mapping imbriqué sur de grandes listes

Exemple concret : Pour 100 créneaux disponibles et 50 périodes occupées, un algorithme O(n²) effectue 5 000 vérifications. Poussez cela à 500 créneaux et 200 périodes occupées, et vous faites 100 000 opérations. Cela représente une charge de calcul 20x plus élevée pour seulement une augmentation de 5x des données.

Choisissez les bonnes structures de données et les bons algorithmes

La plupart des problèmes de performance se résolvent en choisissant de meilleures structures de données et de meilleurs algorithmes :

  • Tri + sortie anticipée : Triez vos données une seule fois, puis sortez des boucles lorsque vous savez que les éléments restants ne correspondront pas

  • Recherche binaire : Utilisez la recherche binaire pour les recherches dans des tableaux triés plutôt que des parcours linéaires

  • Techniques à deux pointeurs : Pour fusionner ou intersecter des séquences triées, parcourez les deux avec des pointeurs au lieu de boucles imbriquées

  • Tables de hachage/ensembles : Utilisez des objets ou des Sets pour des recherches en O(1) au lieu de .find ou .includes sur des tableaux

  • Arbres d’intervalles : Pour la planification, la disponibilité et les requêtes de plages, utilisez des structures arborescentes adaptées plutôt qu’une comparaison brutale

Exemple de transformation :

// 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;
});

Vérifications automatisées des performances

Nous allons mettre en place plusieurs niveaux de défense contre les régressions de performance :

Règles de linting qui signalent :

  • Les fonctions avec des boucles imbriquées ou des méthodes de tableau imbriquées

  • Plusieurs appels imbriqués à .some, .find ou .filter

  • La récursion sans mémoïsation

  • Les anti-modèles connus pour notre domaine (planification, vérifications de disponibilité, etc.)

Benchmarks de performance dans CI qui :

  • Exécutent les algorithmes critiques sur des données réalistes et à grande échelle

  • Comparent les temps d’exécution à une base de référence à chaque PR

  • Bloquent les merges qui introduisent des régressions de performance

  • Testent avec des données à l’échelle Enterprise (des milliers d’utilisateurs, des dizaines de milliers d’enregistrements)

Surveillance en production qui :

  • Suit le temps d’exécution des chemins critiques

  • Alerte lorsque les algorithmes ralentissent à mesure que les données augmentent

  • Détecte les régressions avant que les utilisateurs ne les remarquent

  • Fournit des données de performance réelles pour orienter les optimisations

La performance est une fonctionnalité

La performance n’est pas optionnelle. Ce n’est pas quelque chose que nous « remettons à plus tard ». Pour les clients enterprise qui réservent au sein de grandes équipes, des réponses lentes signifient une baisse de productivité et des utilisateurs frustrés (notre expérience avec certains grands clients enterprise peut en témoigner).

Chaque ingénieur devrait :

  • profiler votre code avant d’optimiser, mais penser à la complexité dès le départ

  • tester avec des données réalistes et à grande échelle (pas seulement 5 enregistrements de test). Nous avons déjà des scripts de seed. Nous devons probablement les étendre.

  • choisir d’emblée des algorithmes et des structures de données efficaces

  • surveiller les itérations imbriquées lors de la revue de code

  • remettre en question tout algorithme qui évolue selon le produit de deux variables

La réalité NP-difficile de la planification

Les problèmes de planification sont fondamentalement NP-difficiles. Cela signifie que lorsque le nombre de contraintes, de participants ou de créneaux horaires augmente, la complexité computationnelle peut exploser de façon exponentielle. La plupart des algorithmes de planification optimaux ont une complexité temporelle exponentielle dans le pire cas, ce qui rend le choix de l’algorithme absolument critique.

Implications concrètes :

  • Trouver l’heure de réunion optimale pour 10 personnes réparties sur 3 fuseaux horaires avec des contraintes de disponibilité individuelles est coûteux en calcul

  • Ajouter la détection des conflits, des buffers et une multitude d’autres options amplifie le problème

  • De mauvais choix d’algorithmes, qui fonctionnent très bien pour de petites équipes, deviennent totalement inutilisables pour les grandes organisations

  • Ce qui prend des millisecondes pour 5 utilisateurs peut prendre de nombreuses secondes pour les organisations

Stratégies pour gérer la complexité NP-difficile :

  • Utiliser des algorithmes d’approximation qui trouvent rapidement des solutions « suffisamment bonnes » plutôt que lentement des solutions parfaites

  • Mettre en place une mise en cache agressive des plannings et des disponibilités calculés

  • Pré-calculer les scénarios courants pendant les heures creuses

  • Diviser les grands problèmes de planification en blocs plus petits et plus faciles à gérer

  • Définir des délais d’attente raisonnables et revenir à des algorithmes plus simples si nécessaire

C’est pourquoi la performance n’est pas seulement un plus dans les logiciels de planification. C’est la base qui détermine si votre système peut évoluer pour répondre aux besoins enterprise ou s’effondrer sous des schémas d’utilisation réels.

Exigences de couverture du code

  • Suivi global de la couverture

    • Nous suivons la couverture globale de la base de code comme un indicateur clé qui s’améliore au fil du temps. Cela nous donne de la visibilité sur notre maturité en matière de tests et aide à identifier les zones qui nécessitent une attention particulière. Le pourcentage global de couverture est affiché bien en évidence dans nos tableaux de bord.
  • 80 %+ de couverture pour le nouveau code

    • Chaque PR doit avoir une couverture de test proche de 80 %+ pour le code qu’elle introduit ou modifie. C’est appliqué automatiquement dans notre pipeline CI. Si vous ajoutez 50 lignes de nouveau code, ces 50 lignes doivent être couvertes par des tests. Si vous modifiez une fonction existante, vos changements doivent être testés. Il s’agit de la couverture de test globale. La couverture des tests unitaires doit être proche de 100 %, surtout avec la possibilité de tirer parti de l’IA pour aider à les générer.

Répondre à l’argument « la couverture ne raconte pas toute l’histoire » : Oui, nous savons que la couverture ne garantit pas des tests parfaits. Nous savons que vous pouvez écrire des tests insignifiants qui couvrent chaque ligne sans rien tester de pertinent. Nous savons que la couverture n’est qu’un indicateur parmi d’autres. Mais il vaut sûrement mieux viser un pourcentage élevé que de ne pas savoir du tout où vous en êtes.

Mesurer le succès

  • « Vélocité » (emprunté à Scrum même si nous n’utiliserons pas Scrum)

    • Croissance continue des statistiques mensuelles (fonctionnalités, améliorations, refactorisations)
  • Qualité

    • Réduire l’effort de PR consacré aux corrections de 35 % actuellement à 20 % ou moins d’ici fin 2026 (calculé à partir des modifications de fichiers et des ajouts/suppressions)
  • Santé architecturale

    • Métriques sur l’adhésion aux patterns, le couplage technologique, les violations de frontières
  • Efficacité de la revue

    • Des PR plus petites, des revues plus rapides, moins d’allers-retours de feedback
  • Disponibilité de l’application et de l’API

    • À quel point sommes-nous proches de 99.99 % ?
Keith Williams

Post by

Keith Williams

Head of Engineering