Blog

Engineering in 2026 en daarna

We bouwen infrastructuur die bijna nooit mag uitvallen. Om dat te bereiken, werken we snel terwijl we geweldige software van hoge kwaliteit leveren zonder shortcuts of compromissen. Dit document schetst de technische standaarden die ons door 2026 en daarna zullen begeleiden.

Keith WilliamsOct 14, 2025

Teamsamenstelling

Onze engineeringorganisatie bestaat uit vijf kernteams, elk met eigen verantwoordelijkheden:

  • Foundation Team: Richt zich op het vaststellen en onderhouden van coderingsstandaarden en architectuurpatronen. Dit team werkt nauw samen met andere teams om best practices binnen de organisatie te introduceren.

  • Consumer-, Enterprise- en Platform-teams: Productgerichte teams die functies snel shippen terwijl ze de kwaliteitsstandaarden uit dit document handhaven. Deze teams bewijzen dat snelheid en kwaliteit elkaar niet uitsluiten.

  • Community Team: Verantwoordelijk voor het snel reviewen van PR's uit de open source-community, feedback geven en dat werk begeleiden tot aan merge. Dit team zorgt ervoor dat onze open source-bijdragers een geweldige ervaring hebben en dat hun bijdragen aan onze kwaliteitsstandaarden voldoen. Het onderhouden van een sterke open-source-cultuur blijft bepalen hoe Cal.com engineering-samenwerking en productontwikkeling aanpakt.

Onze resultaten tot nu toe

De cijfers spreken voor zich. In het afgelopen anderhalf jaar hebben we de manier waarop we software bouwen fundamenteel veranderd:

Screenshot 2025-10-01 at 9.40.36 PM.png

We hebben onze engineering-output ongeveer verdubbeld terwijl we tegelijkertijd de kwaliteit hebben verbeterd. Nog indrukwekkender is de verschuiving in wat we bouwen:

Screenshot 2025-10-01 at 9.41.51 PM.png

We hebben met succes ongeveer 20% van de engineeringinzet verplaatst van fixes naar features, prestatieverbeteringen, refactors en klusjes. Deze verschuiving laat zien dat investeren in kwaliteit en architectuur je niet vertraagt. Het versnelt je.

Cal.coms basis maakt excellentie mogelijk voor coss.com

  • Cal.com is een stabiel, winstgevend bedrijf dat we blijven laten groeien.

    • Dit succes geeft ons een uniek voordeel terwijl we coss.com bouwen. Anders dan in de beginjaren van Cal.com, toen we snel moesten bewegen om product-market fit te bereiken en een duurzaam bedrijf op te bouwen, start coss.com vanuit een sterke positie.
  • We hoeven niet te haasten met coss.com.

    • De stabiliteit van Cal.com betekent dat we ons kunnen veroorloven om coss.com vanaf dag één op de juiste manier te bouwen. We hebben de luxe om deze engineeringstandaarden te implementeren zonder de druk van directe marktvraag of financieringsbeperkingen. Dat is een fundamenteel andere startpositie.
  • De "traagheid" is een investering, geen kostenpost.

    • Ja, het volgen van deze standaarden kan in eerste instantie trager voelen en voor sommige engineers zelfs frustrerend zijn. DTO's schrijven kost meer tijd dan databasetypes direct doorgeven aan de frontend. Goede abstracties maken en dependency injection gebruiken vraagt om meer ontwerp vooraf. Voor nieuwe code 80%+ testdekking behouden vraagt discipline. Maar deze schijnbare traagheid is tijdelijk, en de opbrengst is exponentieel.

Denk aan het samengestelde rendement...

  • Code die vanaf het begin goed is gearchitecteerd, heeft later geen enorme refactors nodig

  • Hoge testdekking voorkomt bugs die anders weken aan debugging en hotfixes zouden kosten (zie 2023 tot midden 2024)

  • Goede abstracties maken het op termijn dramatisch sneller om nieuwe features toe te voegen

  • Schone grenzen en DTO's voorkomen de architecturale erosie die uiteindelijk complete herschrijvingen vereist

Het Cal.com traject laat zien wat er gebeurt als je optimaliseert voor directe snelheid. Hoge startsnelheid die geleidelijk afneemt naarmate technische schuld zich opstapelt, architecturale shortcuts bottlenecks creëren en er meer tijd wordt besteed aan het oplossen van problemen dan aan het bouwen van features (zie de vorige grafiek waarin we 55-60% van de engineeringinzet aan fixes besteedden).

Het coss.com-traject zal de kracht omarmen van goed bouwen vanaf dag één. Iets tragere initiële snelheid terwijl goede patronen worden neergezet, gevolgd door exponentiële versnelling naarmate die patronen hun waarde bewijzen en snellere ontwikkeling met meer vertrouwen mogelijk maken.

Kernprincipes

1. Geen uitgestelde kwaliteit

  • We minimaliseren "ik doe dit wel in een follow-up PR" voor kleine refactors.

    • Follow-up PR's voor kleine verbeteringen komen zelden echt van de grond. In plaats daarvan stapelen ze zich op als technische schuld die ons maanden of jaren later belast. Als een kleine refactor nu kan worden gedaan, doe hem dan nu. Follow-ups zijn alleen bedoeld voor substantiële wijzigingen die echt een aparte PR rechtvaardigen of voor uitzonderlijke, urgente gevallen.

2. Hoge standaarden in code review

  • Laat PR's niet door met een hoop nits alleen maar om niet "de vervelende persoon" te zijn.

    • Zo worden codebases juist slordig na verloop van tijd. Code review gaat niet om aardig zijn. Het gaat om het bewaken van de kwaliteitsstandaarden die onze infrastructuur vereist. Elke kleine opmerking telt. Elke schending van een patroon telt. Los ze op vóór merge, niet erna.

3. Duw elkaar om het juiste te doen

  • We houden elkaar verantwoordelijk voor kwaliteit

    • Snel shortcuts nemen lijkt op het moment zelf misschien sneller, maar het veroorzaakt problemen die later iedereen vertragen. Als je ziet dat een teammate op het punt staat een PR met duidelijke problemen te mergen, trek dan aan de bel. Als iemand een snelle hack voorstelt in plaats van de juiste oplossing, geef dan tegengas. Als je in de verleiding komt om tests over te slaan of architectuurpatronen te negeren, verwacht dan dat je teammates je daarop aanspreken.
  • Dit gaat niet over moeilijk doen of mensen vertragen

    • Het gaat om collectief eigenaarschap van onze codebase en onze reputatie. Elke shortcut die één persoon neemt, wordt ieders probleem. Elke afsnijding vandaag betekent morgen meer debug-sessies, meer hotfixes en meer gefrustreerde klanten.
  • Maak het normaal om slechte beslissingen respectvol uit te dagen

    • Als iemand zegt: "laten we dit voorlopig gewoon hardcoden", zou het verwachte antwoord moeten zijn: "wat zou het kosten om het de eerste keer op de juiste manier te doen?" Als iemand ongeteste code wil committen, moet het team daartegen ingaan. Als iemand voorstelt om te kopiëren en plakken in plaats van een goede abstractie te maken, spreek dat dan respectvol aan.
  • We bouwen iets dat bijna nooit mag uitvallen

    • Dat niveau van betrouwbaarheid ontstaat niet toevallig. Het gebeurt wanneer elke engineer zich verantwoordelijk voelt voor kwaliteit, niet alleen voor zijn of haar eigen code maar voor het hele systeem. We slagen als team of we falen als team.

4. Ga voor eenvoud

  • Geef prioriteit aan helderheid boven slimdoenerij

    • Het doel is code die makkelijk en snel te lezen en te begrijpen is, niet elegante complexiteit. Simpele systemen verlagen de cognitieve belasting voor elke engineer.
  • Stel jezelf de juiste vragen

    • Los ik echt het probleem op dat voor me ligt?

    • Denk ik niet te veel aan mogelijke toekomstige use cases?

    • Heb ik minstens 1 ander alternatief overwogen om dit op te lossen? Hoe verhoudt dat zich?

  • Simpel betekent niet dat er functies ontbreken

    • Alleen omdat ons doel is om eenvoudige systemen te creëren, betekent dat niet dat ze uitgekleed en zonder duidelijke functionaliteit moeten aanvoelen.

5. Automatiseer alles

  • Maak gebruik van AI

    • Genereer 80% van de boilerplate en niet-kritische code met AI, zodat we ons volledig kunnen richten op complexe business logic en kritieke architecturen. Teams die moderne AI-ondersteunde workflows omarmen, vertrouwen steeds vaker op tools zoals de best MCP servers om developerproductiviteit en orkestratie te verbeteren.

    • Bouw zero-noise alerting en slimme foutafhandeling.

    • Handmatig testen wordt steeds meer verleden tijd. AI kan snel en slim megatest-suites voor ons bouwen.

  • Onze CI is de eindbaas

    • Alles in dit standaarden document wordt gecontroleerd voordat code in PR's wordt gemerged

    • Geen verrassingen halen main

    • Checks zijn snel en nuttig

Architectuurstandaarden

We stappen over op een streng architectuurmodel op basis van Vertical Slice Architecture en Domain-Driven Design (DDD). De volgende patronen en principes worden strikt afgedwongen in PR-reviews en via linting.

Vertical Slice Architecture: packages/features

Onze codebase is georganiseerd op domein, niet op technische laag. De map packages/features vormt de kern van deze architecturale aanpak. Elke map daarin vertegenwoordigt een complete verticale slice van de applicatie, gestuurd door het domein dat het raakt.

Structuur:

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

Elke featuremap is een op zichzelf staande verticale slice die alles bevat wat nodig is voor dat domein:

  • Domeinlogica: De kern van business rules en entiteiten die specifiek zijn voor die feature

  • Applicatieservices: Orkestratie van use cases voor dat domein

  • Repositories: Datatoegang specifiek voor de behoeften van die feature

  • DTO's: Data transfer objects voor het overbruggen van grenzen

  • UI-componenten: Frontendcomponenten die bij deze feature horen (waar van toepassing)

  • Tests: Unit-, integratie- en e2e-tests voor deze feature

Waarom Vertical Slices belangrijk zijn

Traditionele gelaagde architectuur organiseert op technische concerns:

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

Dat veroorzaakt meerdere problemen:

  • Wijzigingen aan één feature vereisen dat je bestanden aanraakt die verspreid staan over meerdere mappen

  • Het is lastig te begrijpen wat een functie doet, omdat de code gefragmenteerd is

  • Teams zitten elkaar in de weg wanneer ze aan verschillende functies werken

  • Je kunt een functie niet makkelijk uitlichten of afschaffen

Vertical slice-architectuur organiseert per domein:

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

Dit lost deze problemen op:

  • Alles wat met beschikbaarheid te maken heeft, staat in packages/features/availability

  • Je kunt de hele beschikbaarheidsfunctie begrijpen door één map te verkennen

  • Teams kunnen aan verschillende functies werken zonder conflicten (als het engineeringteam van Cal.com groeit, maar in coss.com zullen we zeker teams grote packages laten oppakken)

  • Functies zijn losjes gekoppeld en kunnen onafhankelijk evolueren

Richtlijnen voor functieorganisatie

In theorie is elke functie onafhankelijk uitrolbaar. Ook al rollen we ze misschien niet echt apart uit, dwingt deze manier van organiseren ons om afhankelijkheden duidelijk te houden en koppeling minimaal te houden. Dit is het uitgangspunt en het succes van microservices, hoewel we nog niet microservices zullen uitrollen.

Functies communiceren via duidelijk gedefinieerde interfaces. Als bookings beschikbaarheidsgegevens nodig heeft, importeert het die uit @calcom/features/availability via geëxporteerde interfaces, niet door in interne implementatiedetails te graven.

Gedeelde code staat op de juiste plekken:

  • Domeinonafhankelijke utilities en cross-cutting concerns (auth, logging): packages/lib

  • Gedeelde UI-primitives: packages/ui (en binnenkort coss.com ui)

Domeingrenzen worden automatisch afgedwongen. We gaan linting bouwen die voorkomt dat je in de interne delen van functies komt waar je niet in mag komen. Als packages/features/bookings probeert te importeren uit packages/features/availability/services/internal, blokkeert de linter dat. Alle afhankelijkheden tussen functies moeten via de publieke API van de functie lopen.

good_architecture_diagram.pngbad_architecture_diagram.png

Nieuwe functies beginnen als verticale slices. Als je iets nieuws bouwt, maak dan een nieuwe map in packages/features met de complete verticale slice. Zo is meteen duidelijk wat je bouwt en blijft alles vanaf dag één georganiseerd.

Voordelen

  • Vindbaarheid

    • Op zoek naar booking-logica? Die staat helemaal in packages/features/bookings. Je hoeft niet door controllers, services, repositories en utilities te zoeken die verspreid zijn over de codebase.
  • Makkelijker testen

    • Test de hele functie als één geheel. Je hebt alle onderdelen op één plek, waardoor integratietesten natuurlijk en eenvoudig worden.
  • Duidelijkere afhankelijkheden

    • Als je import { getAvailability } from '@calcom/features/availability' ziet, weet je precies van welke functie je afhankelijk bent. Als afhankelijkheden te complex worden, is dat duidelijk en kun je het aanpakken.

Repositorypatroon en dependency injection

Technologiekeuzes mogen niet door de hele applicatie heen sijpelen. Het Prisma-probleem laat dit perfect zien. Op dit moment hebben we verwijzingen naar Prisma verspreid over honderden bestanden. Dat zorgt voor enorme koppeling en maakt technologische wijzigingen onbetaalbaar duur. We voelen nu de pijn daarvan terwijl we Prisma upgraden naar v6.16. Iets dat eigenlijk gewoon een lokale refactor achter afgeschermde repositories had moeten zijn, is een meanderende, bijna eindeloze jacht op issues geworden over meerdere apps heen.

De standaard vanaf nu:

  • Alle database-toegang moet via Repository-klassen lopen. We hebben hier al een mooie voorsprong in.

  • Repositories zijn de enige code die iets weet van Prisma (of een andere ORM). Er mag geen logica in zitten.

  • Repositories worden geïnjecteerd via Dependency Injection-containers

  • Als we ooit overstappen van Prisma naar Drizzle of een andere ORM, zijn de enige vereiste wijzigingen:

    • Implementaties van repositories

    • DI-containerbedrading voor nieuwe repositories

    • De rest van de codebase mag nergens om geven of veranderen

Dit is niet theoretisch. Zo bouwen we onderhoudbare systemen.

Data Transfer Objects (DTOs)

Databasetypes mogen niet lekken naar de frontend. Dit is in onze tech stack een populaire shortcut geworden, maar het is een code smell die meerdere problemen veroorzaakt.

  • Technologische koppeling (Prisma-types belanden in React-componenten)

  • Beveiligingsrisico's (per ongeluk lekken van gevoelige velden)

  • Breekbare contracten tussen server en client (vooral problematisch nu we veel meer API's bouwen)

  • Niet in staat zijn om het databaseschema onafhankelijk te laten evolueren

  • Alle DTO's-conversies via Zod, zelfs voor een API-response, om te zorgen dat alle data gevalideerd wordt vóór die naar de gebruiker wordt gestuurd. Liever falen dan iets verkeerds teruggeven.

De standaard vanaf nu:
Maak expliciete DTO's op elke architectonische grens.

  1. Datalaag → applicatielaag → API: transformeer databasemodellen naar DTO's van de applicatielaag en transformeer daarna applicatie-DTO's naar API-specifieke DTO's

  2. API → applicatielaag → datalaag: transformeer API-DTO's via de applicatielaag naar dataspecifieke DTO's

Ja, dit vraagt om meer code. Ja, dat is het waard. Expliciete grenzen voorkomen de architectonische erosie die onderhoudsnachtmerries op de lange termijn veroorzaakt.

Patronen voor domain-driven design

De volgende patronen moeten correct en consequent worden gebruikt:

  • Application Services

    • Orkestreren use cases en coördineren tussen domeinservices en repositories
  • Domain Services

    • Bevatten businesslogica die niet natuurlijk bij één entiteit hoort
  • Repositories

    • Abstraheren datatoegang en isoleren technologische keuzes
  • Dependency Injection

    • Maakt losse koppeling mogelijk, vergemakkelijkt testen, isoleert verantwoordelijkheden
  • Caching Proxies

    • Wikkelen repositories of services in om cachinggedrag transparant toe te voegen

    • Natuurlijk niet de enige manier om caching te doen, maar wel een goed startpunt

  • Decorators

    • Voegen cross-cutting concerns toe (logging, metrics, enz.) zonder de domeinlogica te vervuilen

Consistentie van de codebase

Onze codebases moeten aanvoelen alsof één persoon ze heeft geschreven. Dit niveau van consistentie vraagt om strikte naleving van vastgelegde patronen, uitgebreide lintregels die architecturale standaarden afdwingen, code reviews die patroonverstoringen afwijzen + de hulp van AI-code reviewers.

Zet conditionals naar het application entry point

if-statements horen bij het entry point, niet verspreid door je services heen. Dit is een van de belangrijkste architecturale principes om schone, gefocuste code te behouden die niet uitwaaiert naar onhoudbare complexiteit.

Zo verloopt code in de loop van de tijd: een service wordt geschreven voor een duidelijk, specifiek doel. De logica is schoon en gefocust. Dan komt er een nieuwe productvereiste bij en iemand voegt een if-statement toe. Een paar jaar en nog een paar vereisten later staat die service vol met voorwaardelijke checks voor verschillende scenario's. De service is geworden:

  • Complex en moeilijk leesbaar

  • Moeilijk te begrijpen en te beredeneren

  • Gevoeliger voor bugs

  • In strijd met single responsibility (te veel verschillende gevallen afhandelen)

  • Bijna onmogelijk om grondig te testen

De service is zijn boekje te buiten gegaan qua verantwoordelijkheden en logica.

Een oplossing: Factory Pattern met gespecialiseerde services
Gebruik het factory pattern om beslissingen te nemen op het entry point en geef daarna door aan gespecialiseerde services die hun eigen logica afhandelen zonder conditionals.

Voorbeeld uit onze codebase:
De BillingPortalServiceFactory bepaalt of billing voor een organisatie, team of individuele gebruiker is, en geeft daarna de juiste service terug:

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

Elke service handelt daarna zijn specifieke logica af zonder te hoeven checken "ben ik een org of een 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
}

Waarom dit belangrijk is

  • Services blijven gefocust

    • Elke service heeft één verantwoordelijkheid en hoeft niets te weten van andere contexten. De OrganizationBillingPortalService bevat geen if-statements die if (isTeam) of if (isUser) checken. Die weet alleen hoe hij organisaties moet afhandelen.
  • Wijzigingen blijven geïsoleerd

    • Wanneer je organisatiebillinglogica moet aanpassen, raak je alleen OrganizationBillingPortalService aan. Je loopt niet het risico team- of gebruikersbilling te breken. Je hoeft niet door geneste condities heen te volgen om te zien welk pad je code neemt.
  • Testen is eenvoudig

    • Test elke service los, met de specifieke scenario's. Je hoeft niet elke combinatie van condities over verschillende contexten heen te testen.
  • Nieuwe vereisten vervuilen bestaande code niet

    • Bijvoorbeeld: wanneer je enterprisebilling met andere regels moet toevoegen, maak je EnterpriseBillingPortalService. De factory krijgt er één extra conditie bij, maar bestaande services blijven onaangetast en gericht.

Hoe je dit bereikt

  • Verplaats condities naar controllers, factories of routinglogica. Laat deze ingangen beslissen welke service je gebruikt.

  • Houd services zuiver en gericht op één verantwoordelijkheid. Als een service moet checken "welk type ben ik?", heb je waarschijnlijk meerdere services nodig.

  • Kies polymorfisme boven condities

    • Interfaces definiëren het contract. Concrete implementaties leveren de details.
  • Let op opstapeling van if-statements

    • Zie je tijdens code review dat een service condities krijgt voor verschillende scenario's, dan is dat een signaal om te refactoren naar gespecialiseerde services.

API-ontwerp: dunne controllers en HTTP-abstratie

  • Controllers zijn dunne lagen die alleen HTTP-zaken afhandelen.

    • Ze ontvangen requests, verwerken ze en mappen data naar DTO's die worden doorgegeven aan de kernlogica van de applicatie. Vanaf nu mag er geen applicatie- of kernlogica meer zichtbaar zijn in API-routes of tRPC-handlers.
  • We moeten HTTP-technologie loskoppelen van onze applicatie.

    • De manier waarop we data tussen client en server uitwisselen (of dat nu REST, tRPC, enz. is) mag niet bepalen hoe onze kernapplicatie werkt. HTTP is een leveringsmechanisme, geen architecturale drijver.

Verantwoordelijkheden van controllers (en ALLEEN deze):

  • Binnenkomende requests ontvangen en valideren

  • Data uit requestparameters, body en headers halen

  • Requestdata omzetten naar DTO's

  • De juiste applicatieservices aanroepen met die DTO's

  • Responses van applicatieservices omzetten naar response-DTO's

  • HTTP-responses retourneren met de juiste statuscodes

Controllers mogen NIET:

  • Bedrijfslogica of domeinregels bevatten

  • Direct databases of externe services benaderen

  • Complexe datatransformaties of berekeningen uitvoeren

  • Beslissingen nemen over wat de applicatie moet doen

  • In detail weten hoe het domein is geïmplementeerd

Voorbeeld van een dun controllerpatroon:

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

API-versionering en brekende wijzigingen

Geen brekende wijzigingen. Dit is cruciaal. Zodra een API-endpoint publiek is, moet het stabiel blijven. Brekende wijzigingen vernietigen het vertrouwen van ontwikkelaars en zorgen voor integratiehoofdpijn voor onze gebruikers.

Strategieën om brekende wijzigingen te vermijden:

  • Voeg nieuwe velden altijd als optioneel toe

  • Gebruik API-versionering wanneer je bestaand gedrag moet veranderen

  • Deprecate oude endpoints op een nette manier met duidelijke migratiepaden

  • Houd achterwaartse compatibiliteit minstens twee majorversies in stand

Wanneer je toch brekende wijzigingen moet doorvoeren:

  • Maak een nieuwe API-versie met de datumgebaseerde versionering in API v2 (misschien kijken we ook naar de genummerde versionering die Stripe onlangs heeft geïntroduceerd)

  • Laat beide versies tegelijk draaien tijdens de overgang (dat doen we al in API v2)

  • Bied waar mogelijk geautomatiseerde migratietools aan

  • Geef gebruikers ruim de tijd om te migreren (minstens 6 maanden voor publieke API's)

  • Documenteer precies wat er is veranderd en waarom

Prestaties en algoritmische complexiteit

We bouwen voor grote organisaties en teams. Wat prima werkt met 10 gebruikers of 50 records kan instorten onder enterprise-schaal. Prestaties zijn niet iets dat we later optimaliseren. Het is iets dat we vanaf het begin goed bouwen.

Denk vanaf dag één aan schaal

Vraag je bij het bouwen van features altijd af: "Hoe gedraagt dit zich met 1.000 gebruikers? 10.000 records? 100.000 bewerkingen?" Het verschil tussen O(n)- en O(n²)-algoritmen is in ontwikkeling misschien nauwelijks merkbaar, maar in productie catastrofaal.

Veelvoorkomende O(n²)-patronen om te vermijden:

  • Geneste array-iteraties (.map binnen .map, .forEach binnen .forEach)

  • Array-methoden zoals .some, .find of .filter binnen lussen of callbacks

  • Elk item tegen elk ander item controleren zonder optimalisatie

  • Gekoppelde filters of geneste mapping over grote lijsten

Praktijkvoorbeeld: Voor 100 beschikbare slots en 50 bezette periodes voert een O(n²)-algoritme 5.000 checks uit. Schaal dat op naar 500 slots en 200 bezette periodes, en je doet 100.000 bewerkingen. Dat is een 20x toename in rekenlast voor slechts een 5x toename in data.

Kies de juiste datastructuren en algoritmen

De meeste prestatieproblemen los je op door betere datastructuren en algoritmen te kiezen:

  • Sorteren + vroeg stoppen: Sorteer je data één keer en breek dan uit lussen zodra je weet dat de resterende items niet zullen matchen

  • Binaire zoekmethode: Gebruik binary search voor zoekopdrachten in gesorteerde arrays in plaats van lineaire scans

  • Two-pointer-technieken: Voor het samenvoegen of snijden van gesorteerde reeksen loop je met twee pointers door beide in plaats van geneste lussen

  • Hashmaps/sets: Gebruik objecten of Sets voor O(1)-opzoekingen in plaats van .find of .includes op arrays

  • Intervalbomen: Gebruik voor scheduling, beschikbaarheid en range-queries de juiste boomstructuren in plaats van brute-force vergelijkingen

Voorbeeldtransformatie:

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

Geautomatiseerde prestatiechecks

We implementeren meerdere verdedigingslagen tegen prestatieregressies:

Lintregels die markeren:

  • Functies met geneste lussen of geneste array-methoden

  • Meerdere geneste .some, .find of .filter-aanroepen

  • Recursie zonder memoization

  • Bekende antipatronen voor ons domein (scheduling, beschikbaarheidschecks, enz.)

Prestatiebenchmarks in CI die:

  • Kritieke algoritmen draaien op realistische data op grote schaal

  • Uitvoertijden bij elke PR vergelijken met de baseline

  • Merges blokkeren die prestatieregressies introduceren

  • Testen met data op enterpriseschaal (duizenden gebruikers, tienduizenden records)

Productiemonitoring die:

  • Uitvoeringstijd voor kritieke paden bijhoudt

  • Waarschuwt wanneer algoritmen trager worden naarmate data groeit

  • Regresies opvangt voordat gebruikers het merken

  • Reële prestatiegegevens levert om optimalisaties op te baseren

Prestaties zijn een feature

Prestaties zijn niet optioneel. Het is niet iets waar we "later wel aan toekomen." Voor enterprise-klanten die over grote teams heen boeken, betekent trage respons verloren productiviteit en gefrustreerde gebruikers (onze ervaring met sommige grotere enterprise-klanten kan daar getuige van zijn).

Elke engineer zou moeten:

  • Je code profileren voordat je gaat optimaliseren, maar vanaf het begin al nadenken over complexiteit

  • Testen met realistische data op grote schaal (niet alleen 5 testrecords). We hebben al ingebouwde seed-scripts. Die moeten we waarschijnlijk uitbreiden.

  • Efficiënte algoritmes en datastructuren vooraf kiezen

  • Letten op geneste iteraties in code review

  • Elke algoritme in twijfel trekken dat schaalt met het product van twee variabelen

De NP-harde realiteit van planning

Planningsproblemen zijn fundamenteel NP-hard. Dat betekent dat naarmate het aantal beperkingen, deelnemers of tijdsloten groeit, de computationele complexiteit exponentieel kan exploderen. De meeste optimale planningsalgoritmes hebben in het slechtste geval een exponentiële tijdscomplexiteit, waardoor de keuze van het algoritme absoluut cruciaal is.

Gevolgen in de praktijk:

  • Het vinden van het optimale vergadermoment voor 10 mensen verspreid over 3 tijdzones met individuele beschikbaarheidsbeperkingen is computationeel duur

  • Het toevoegen van conflictdetectie, buffers en een hele reeks andere opties vergroot het probleem

  • Slechte algoritmekeuzes die prima werken voor kleine teams, worden totaal onbruikbaar voor grote organisaties

  • Iets wat voor 5 gebruikers milliseconden kost, kan voor organisaties vele seconden duren

Strategieën om NP-harde complexiteit te beheersen:

  • Gebruik benaderingsalgoritmes die snel "goed genoeg"-oplossingen vinden in plaats van langzaam perfecte oplossingen

  • Implementeer agressieve caching van berekende planningen en beschikbaarheid

  • Precompute veelvoorkomende scenario's tijdens daluren

  • Splits grote planningsproblemen op in kleinere, beter beheersbare stukken

  • Stel redelijke time-outlimieten in en val terug op eenvoudigere algoritmes wanneer dat nodig is

Dit is waarom prestaties in planningssoftware niet zomaar een leuke extra zijn. Het is de basis die bepaalt of je systeem kan opschalen naar enterprise-behoeften of bezwijkt onder gebruikspatronen uit de praktijk.

Vereisten voor codecoverage

  • Wereldwijde coverage-tracking

    • We volgen de totale coverage van de codebase als een belangrijke metric die in de loop van de tijd verbetert. Zo krijgen we inzicht in onze testvolwassenheid en kunnen we gebieden identificeren die aandacht nodig hebben. Het globale coverage-percentage wordt prominent weergegeven in onze dashboards.
  • 80%+ coverage voor nieuwe code

    • Elke PR moet bijna 80%+ testcoverage hebben voor de code die die toevoegt of wijzigt. Dit wordt automatisch afgedwongen in onze CI-pipeline. Als je 50 regels nieuwe code toevoegt, moeten die 50 regels door tests worden afgedekt. Als je een bestaande functie wijzigt, moeten je wijzigingen getest worden. Dit is de totale testcoverage. Unit testcoverage moet bijna 100% zijn, zeker met de mogelijkheid om AI in te zetten om die te helpen genereren.

De claim "coverage vertelt niet het hele verhaal" beantwoorden: Ja, we weten dat coverage geen perfecte tests garandeert. We weten dat je betekenisloze tests kunt schrijven die elke regel raken maar niets wezenlijks testen. We weten dat coverage slechts één metric is tussen vele. Maar het is zeker beter om te mikken op een hoog percentage dan helemaal geen idee te hebben waar je staat.

Succes meten

  • "Velocity" (dit stelen we van Scrum, ook al gaan we Scrum niet gebruiken)

    • Voortdurende groei in maandelijkse statistieken (features, verbeteringen, refactors)
  • Kwaliteit

    • Verminder de PR-inspanning die aan fixes wordt besteed van de huidige 35% naar 20% of lager tegen eind 2026 (berekend op basis van bestandswijzigingen en toevoegingen/verwijderingen)
  • Architecturale gezondheid

    • Metrics over naleving van patronen, technologische koppeling en schendingen van grenzen
  • Reviewefficiëntie

    • Kleinere PR's, snellere reviews, minder feedbackrondes
  • Uptime van de applicatie en API

    • Hoe dicht zitten we bij 99.99%?
Keith Williams

Post by

Keith Williams

Head of Engineering