Unternehmen

Vorlagen

Preisgestaltung

Unternehmen

Vorlagen

Preisgestaltung

Durch

Keith Williams

14.10.2025

Ingenieurwesen im Jahr 2026 und darüber hinaus

Ingenieurwesen im Jahr 2026 und darüber hinaus

Ingenieurwesen im Jahr 2026 und darüber hinaus

Wir bauen Infrastrukturen, die fast nie ausfallen dürfen. Um dies zu erreichen, arbeiten wir schnell und liefern qualitativ hochwertige Software ohne Abkürzungen oder Kompromisse. Dieses Dokument beschreibt die ingenieurtechnischen Standards, die uns bis 2026 und darüber hinaus leiten werden.

Teamstruktur

Unsere Ingenieurorganisation besteht aus fünf Kernteams, die jeweils unterschiedliche Verantwortungsbereiche haben:

  • Foundation Team: Konzentriert sich auf die Herstellung und Einhaltung von Codierungsstandards und architektonischen Mustern. Dieses Team arbeitet eng mit anderen Teams zusammen, um Best Practices in der gesamten Organisation zu etablieren.

  • Consumer-, Enterprise- und Plattform-Teams: Produktorientierte Teams, die schnell Funktionen bereitstellen und gleichzeitig die in diesem Dokument festgelegten Qualitätsstandards einhalten. Diese Teams beweisen, dass Geschwindigkeit und Qualität sich nicht gegenseitig ausschließen.

  • Community-Team: Zuständig für die schnelle Überprüfung von PRs aus der Open-Source-Community, Bereitstellung von Feedback und Unterstützung dieser Arbeit bis zur Zusammenführung. Dieses Team sorgt dafür, dass unsere Open-Source-Beitragenden ein großartiges Erlebnis haben und ihre Beiträge unseren Qualitätsstandards entsprechen.

Unsere Ergebnisse bisher

Die Daten sprechen für sich. Im letzten Jahr und einem halben Jahr haben wir grundlegend verändert, wie wir Software entwickeln:

Screenshot 2025-10-01 at 9.40.36 PM.png

Wir haben unsere Engineering-Leistung ungefähr verdoppelt, während wir gleichzeitig die Qualität verbessert haben. Noch beeindruckender ist der Wandel, was wir entwickeln:

Screenshot 2025-10-01 at 9.41.51 PM.png

Wir haben erfolgreich etwa 20% des Engineeringsaufwands von Korrekturen umgeleitet hin zu Features, Leistungsverbesserungen, Refaktorierungen und Aufgaben. Diese Verschiebung zeigt, dass Investitionen in Qualität und Architektur einen nicht verlangsamen. Sie beschleunigen einen.

Cal.com's Grundlage ermöglicht Exzellenz für coss.com

  • Cal.com ist ein stabiles, profitables Unternehmen, das wir weiter ausbauen werden.

    • Dieser Erfolg verschafft uns einen einzigartigen Vorteil beim Aufbau von coss.com. Anders als in den frühen Tagen von Cal.com, als wir schnell handeln mussten, um die Produkt-Markt-Passung zu erreichen und ein nachhaltiges Geschäft aufzubauen, startet coss.com aus einer Position der Stärke.

  • Wir müssen uns bei coss.com nicht beeilen.

    • Die Stabilität von Cal.com bedeutet, dass wir uns leisten können, coss.com von Anfang an richtig aufzubauen. Wir haben den Luxus, diese technischen Standards umzusetzen, ohne den Druck von sofortigen Marktanforderungen oder Finanzierungsbeschränkungen. Dies ist eine grundlegend andere Ausgangsposition.

  • Die "Langsamkeit" ist eine Investition, keine Kosten.

    • Ja, die Einhaltung dieser Standards mag sich anfangs langsamer anfühlen und für einige Ingenieure sogar frustrierend sein. Das Schreiben von DTOs dauert länger, als Datenbanktypen direkt an das Frontend zu übergeben. Die Erstellung ordnungsgemäßer Abstraktionen und der Einsatz von Abhängigkeitsinjektion erfordert mehr Vorabgestaltung. Eine Testabdeckung von über 80% für neuen Code aufrechtzuerhalten, erfordert Disziplin. Aber diese scheinbare Langsamkeit ist vorübergehend, und die Belohnung ist exponentiell.

Betrachten Sie die verzinslichen Renditen...

  • Richtig von Anfang an architekturierter Code benötigt später keine umfangreichen Refaktorierungen mehr

  • Hohe Testabdeckung verhindert Fehler, die sonst Wochen voller Debugging und Hotfixes erfordern würden (siehe 2023 bis Mitte 2024)

  • Richtige Abstraktionen machen das Hinzufügen neuer Funktionen im Laufe der Zeit erheblich schneller

  • Klar definierte Grenzen und DTOs verhindern die architektonische Erosion, die schließlich vollständige Neuschreibungen erfordert

Der Cal.com-Verlauf zeigt, was passiert, wenn man für unmittelbare Geschwindigkeit optimiert. Hohe Anfangsgeschwindigkeit, die allmählich abnimmt, wenn technologische Schulden sich anhäufen, architektonische Abkürzungen Engpässe schaffen und mehr Zeit mit der Behebung von Problemen verbracht wird, als Funktionen zu entwickeln (siehe früheres Diagramm, in dem wir 55-60% des Engineeringsaufwands für Korrekturen aufgewendet haben).

Der Trajektorie von coss.com wird die Kraft des korrekten Bauens von Anfang an nutzen. Etwas langsamer initiale Geschwindigkeit beim Festlegen ordnungsgemäßer Muster, gefolgt von exponentieller Beschleunigung, während diese Muster Dividenden zahlen und schnellere Entwicklungen mit höherem Vertrauen ermöglichen.

Kernprinzipien

1. Keine aufgeschobene Qualität

  • Wir werden "Ich mache es in einem nachfolgenden PR" bei kleinen Refaktorierungen minimieren.

    • Nicht abgeschlossene PRs für kleinere Verbesserungen finden selten statt. Stattdessen akkumulieren sie sich als technische Schuld, die uns Monate oder Jahre später belastet. Wenn eine kleine Refaktorierung jetzt gemacht werden kann, dann machen Sie sie jetzt. Nachfolgende PRs sollten für wesentliche Änderungen vorbehalten sein, die wirklich separate PRs oder außergewöhnliche, dringende Fälle rechtfertigen.

2. Hohe Standards in Code-Review

  • Lassen Sie keine PRs mit vielen Anmerkungen durch, nur um nicht der "böse Mensch" zu sein.

    • Genau so werden Codebasen im Laufe der Zeit schlampig. Code-Review hat nicht damit zu tun, nett zu sein. Es geht darum, die Qualitätsstandards aufrechtzuerhalten, die unsere Infrastruktur erfordert. Jede Anmerkung zählt. Jede Musterverletzung zählt. Adressieren Sie sie vor dem Merging, nicht danach.

3. Treibt euch gegenseitig dazu an, das Richtige zu tun

  • Wir halten uns gegenseitig für die Qualität verantwortlich

    • Ecken abzuschnipsen, mag sich im Moment schneller anfühlen, aber es schafft Probleme, die später alle verlangsamen. Wenn Sie sehen, dass ein Teamkollege dabei ist, einen PR mit offensichtlichen Problemen zu mergen, sprechen Sie es an. Wenn jemand einen schnellen Hack anstelle der richtigen Lösung vorschlägt, wehren Sie sich. Wenn Sie versucht sind, Tests zu überspringen oder architektonische Muster zu ignorieren, erwarten Sie, dass Ihre Teamkollegen Sie herausfordern.

  • Es geht nicht darum, schwierig zu sein oder Leute zu verlangsamen

    • Es geht um die kollektive Eigentümerschaft unserer Codebasis und unseres Rufs. Jede Abkürzung, die eine Person nimmt, wird zum Problem aller. Jede heute geschnittene Ecke bedeutet mehr Debugging-Sitzungen, mehr Hotfixes und mehr frustrierte Kunden morgen.

  • Machen Sie es normal, schlechte Entscheidungen respektvoll herauszufordern

    • Wenn jemand sagt: "Lass uns das jetzt einfach hart codieren", sollte die erwartete Antwort sein: "Was würde es kosten, es das erste Mal richtig zu machen?" Wenn jemand ungetesteten Code einfügen möchte, sollte das Team zurückdrängen. Wenn jemand vorschlägt, zu kopieren und einzufügen, anstatt eine ordentliche Abstraktion zu schaffen, sprechen Sie es respektvoll an.

  • Wir bauen etwas, das fast niemals scheitern sollte

    • Dieses Maß an Zuverlässigkeit passiert nicht zufällig. Es passiert, wenn jeder Ingenieur sich für die Qualität verantwortlich fühlt, nicht nur für seinen eigenen Code, sondern für das gesamte System. Wir gewinnen als Team oder wir scheitern als Team.

4. Streben Sie nach Einfachheit

  • Priorisieren Sie Klarheit über Cleverness

    • Das Ziel ist Code, der leicht zu lesen und schnell zu verstehen ist, nicht elegante Komplexität. Einfache Systeme reduzieren die kognitive Last für jeden Ingenieur.

  • Stellen Sie sich die richtigen Fragen

    • Löse ich tatsächlich das aktuelle Problem?

    • Denke ich zu viel über mögliche zukünftige Anwendungsfälle nach?

    • Habe ich mindestens eine andere Alternative zur Lösung dieses Problems in Betracht gezogen? Wie vergleicht sie sich?

  • Einfach bedeutet nicht mangelhaft an Funktionen

    • Nur weil unser Ziel es ist, einfache Systeme zu schaffen, bedeutet dies nicht, dass sie dürftig und ohne offensichtliche Funktionalität sein sollten.

5. Automatisiere alles

  • Nutze KI

    • Generiere 80% des Boilerplates und nicht-kritischen Codes mit KI, damit wir uns ausschließlich auf komplexe Geschäftslogik und kritische Architekturen konzentrieren können.

    • Erstellen Sie geräuschfreie Alarme und intelligente Fehlerbehandlung.

    • Manuelles Testen gehört mehr und mehr der Vergangenheit an. KI kann schnell und intelligent Mega-Test-Suites für uns erstellen.

  • Unser CI ist der Endgegner

    • Alles in diesem standardisierten Dokument wird überprüft, bevor Code in PRs zusammengeführt wird

    • Keine Überraschungen gelangen in das Haupt-Repository

    • Prüfungen sind schnell und nützlich

Architekturstil

Wir wechseln zu einem strikten Architekturmodell basierend auf Vertical Slice Architecture und Domain-Driven Design (DDD). Die folgenden Muster und Prinzipien werden in PR-Reviews rigoros durchgesetzt und durch Linting unterstützt.

Vertical Slice Architecture: Pakete/Funktionen

Unsere Codebasis ist nach Domäne organisiert, nicht nach technischer Schicht. Das packages/features-Verzeichnis ist das Herzstück dieses architektonischen Ansatzes. Jedes darin befindliche Verzeichnis stellt einen vollständigen vertikalen Ausschnitt der Anwendung dar, angetrieben durch die betroffene Domäne.

Struktur:


Jeder Funktionsordner ist ein autarker vertikaler Ausschnitt, der alles enthält, was für diese Domäne benötigt wird:

  • Domänenlogik: Die zentralen Geschäftsregeln und Entitäten spezifisch für diese Funktion

  • Anwendungsdienste: Anwendungsfallorchestration für diese Domäne

  • Repositories: Datenzugriff, spezifisch für die Anforderungen dieser Funktion

  • DTOs: Datenübertragungsobjekte zum Überschreiten von Grenzen

  • UI-Komponenten: Frontend-Komponenten, die sich auf diese Funktion beziehen (falls zutreffend)

  • Tests: Unit-, Integrations- und End-to-End-Tests für diese Funktion

Warum vertikale Ausschnitte wichtig sind

Traditionelle geschichtete Architektur ist nach technischen Belangen organisiert:


Das schafft mehrere Probleme:

  • Änderungen an einer Funktion erfordern das Bearbeiten von Dateien, die über mehrere Verzeichnisse verstreut sind

  • Es ist schwierig zu verstehen, was eine Funktion tut, da ihr Code fragmentiert ist

  • Teams treten sich gegenseitig auf die Füße, wenn sie an verschiedenen Funktionen arbeiten

  • Sie können eine Funktion nicht leicht extrahieren oder veraltet machen

Vertikale Ausschnittarchitektur ist nach Domäne organisiert:


Das löst diese Probleme:

  • Alles, was mit Verfügbarkeit zu tun hat, befindet sich in packages/features/availability

  • Sie können die gesamte Verfügbarkeitsfunktion verstehen, indem Sie ein Verzeichnis erkunden

  • Teams können an verschiedenen Funktionen arbeiten, ohne Konflikte zu haben (wenn das Cal.com-Ingenieursteam wächst, aber definitiv bei coss.com werden wir Teams haben, die sich um wichtige Pakete kümmern)

  • Funktionen sind lose gekoppelt und können unabhängig voneinander entwickelt werden

Richtlinien für die Funktionale Organisation

Theoretisch ist jede Funktion unabhängig einsetzbar. Obwohl wir sie möglicherweise nicht tatsächlich separat bereitstellen, erzwingt diese Organisation, dass wir Abhängigkeiten klar halten und die Kopplung minimal bleibt. Dies ist die Prämisse und der Erfolg von Microservices, obwohl wir noch keine Microservices bereitstellen werden.

Funktionen kommunizieren über gut définierte Schnittstellen. Wenn Buchungen Verfügbarkeitsdaten benötigt, importiert es von @calcom/features/availability über exportierte Schnittstellen und greift nicht auf interne Implementierungsdetails zu.

Gemeinsamer Code lebt an geeigneten Orten:

  • Domänenagnostische Dienstprogramme und bereichsübergreifende Bedenken (Authentifizierung, Protokollierung): packages/lib

  • Gemeinsame UI-Grundlagen: packages/ui (und bald auch coss.com ui)

Domänengrenzen werden automatisch durchgesetzt. Wir werden Linting erstellen, das verhindert, in die internen Funktionen vorzudringen, wo es nicht erlaubt ist. Wenn packages/features/bookings versucht, von packages/features/availability/services/internal zu importieren, blockiert der Linter es. Alle bereichsübergreifenden Abhängigkeiten müssen über die öffentliche API der Funktion gehen.

good_architecture_diagram.pngbad_architecture_diagram.png

Neue Funktionen beginnen als vertikale Ausschnitte. Beim Erstellen von etwas Neuem, erstellen Sie einen neuen Ordner in packages/features mit dem kompletten vertikalen Ausschnitt. Dies macht klar, was Sie bauen und hält alles von Tag eins organisiert.

Vorteile

  • Auffindbarkeit

    • Suchen Sie die Buchungslogik? Sie befindet sich alles in packages/features/bookings. Keine Notwendigkeit, durch Controller, Dienste, Repositories und verstreute Hilfsprogramme im Code zu jagen.

  • Einfacheres Testen

    • Testen Sie die gesamte Funktion als eine Einheit. Sie haben alle Teile an einem Ort, was integration tests natürlich und unkompliziert macht.

  • Klarere Abhängigkeiten

    • Wenn Sie sehen import { getAvailability } from '@calcom/features/availability', wissen Sie genau, von welcher Funktion Sie abhängen. Wenn die Abhängigkeiten zu komplex werden, ist es offensichtlich und kann angegangen werden.

Repository-Muster und Abhängigkeitsinjektion

Technologieentscheidungen dürfen sich nicht durch die Anwendung ausbreiten. Das Prisma-Problem illustriert dies perfekt. Derzeit haben wir Referenzen zu Prisma, die über hunderte von Dateien verstreut sind. Dies schafft massive Kopplung und macht Technologieänderungen unhaltbar teuer. Wir erleben jetzt die Schmerzen beim Upgrade von Prisma auf v6.16. Etwas, das nur eine lokalisierte Refaktorierung hinter abgeschirmten Repositories hätte sein sollen, war eine langwierige, nahezu endlose Verfolgung von Problemen über mehrere Apps hinweg.

Der Standard von jetzt an:

  • Alle Datenbankzugriffe müssen über Repository-Klassen erfolgen. Wir haben bereits einen guten Vorsprung dabei.

  • Repositories sind der einzige Code, der von Prisma (oder einem anderen ORM) weiß. Es sollte keine Logik in ihnen sein.

  • Repositories werden über Abhängigkeitsinjektion-Container injiziert

  • Wenn wir jemals von Prisma zu Drizzle oder einem anderen ORM wechseln, sind die einzigen Änderungen erforderlich:

    • Repository-Implementierungen

    • DI-Container-Verkabelung für neue Repositories

    • Nichts anderes im Code-Base sollte sich kümmern oder ändern

Das ist keine Theorie. So bauen wir wartungsfreundliche Systeme auf.

Datenübertragungsobjekte (DTOs)

Datenbanktypen sollten nicht an das Frontend durchdringen. Dies ist zu einer beliebten Abkürzung in unserem Tech-Stack geworden, aber es ist ein Codesmell, das mehrere Probleme schafft.

  • Technologische Koppelung (Prisma-Typen enden in React-Komponenten)

  • Sicherheitsrisiken (versehentliche Weitergabe sensibler Felder)

  • Zerbrechliche Verträge zwischen Server und Client (dies ist besonders problematisch, da wir viele weitere APIs entwickeln)

  • Unfähigkeit, das Datenbankschema unabhängig zu entwickeln

  • Alle DTO-Konvertierungen durch Zod, sogar für eine API-Antwort, um sicherzustellen, dass alle Daten vor dem Senden an den Benutzer validiert werden. Besser, um zu scheitern, als etwas falsch zurückzugeben.

Der Standard von jetzt an:
Erstellen Sie explizite DTOs bei jeder architektonischen Grenze.

  1. Datenebene → Anwendungsebene → API: Transformieren Sie Datenbankmodelle in Anwendungs-DTOs und dann Anwendungs-DTOs in API-spezifische DTOs

  2. API → Anwendungsebene → Datenebene: Transformieren Sie die API-DTOs durch die Anwendungsebene und in daten-spezifische DTOs

Ja, das erfordert mehr Code. Ja, es lohnt sich. Explizite Grenzen verhindern die architektonische Erosion, die langfristige Wartungsalbträume erzeugt.

Domain-Driven Design Patterns

Die folgenden Muster müssen korrekt und konsequent verwendet werden:

  • Anwendungsdienste

    • Orchestrierung von Anwendungsfällen, Koordination zwischen Domänendiensten und Repositories

  • Domänendienste

    • Enthalten Geschäftlogik, die nicht natürlich zu einer einzigen Entität gehört

  • Repositories

    • Abstrahieren von Datenzugang, Isolierung von Technologieentscheidungen

  • Abhängigkeitsinjektion

    • Ermöglichen von loser Koppelung, Erleichterung von Tests, Isolierung von Anliegen

  • Caching-Proxies

    • Umschließen Repositories oder Dienste, um Caching-Verhalten transparent hinzuzufügen

    • Nicht der einzige Weg, Caching zu betreiben, aber ein schöner Ausgangspunkt

  • Dekoratoren

    • Fügen Querschnittsbelange (Protokollierung, Metriken usw.) ohne Verschmutzung der Domänenlogik hinzu

Konsistenz der Codebasis

Unsere Codebasen sollten sich anfühlen, als hätte eine Person sie geschrieben. Dieses Maß an Konsistenz erfordert strikte Einhaltung der etablierten Muster, umfassende Linting-Regeln, die architektonische Standards durchsetzen, Code-Reviews, die Musterverletzungen ablehnen + die Unterstützung von KI-Code-Rezensenten.

Verlagern Sie Bedingungsanweisungen an den Anwendungseingangspunkt

Wenn-Anweisungen gehören an den Eingabepunkt und nicht in Ihren Diensten verstreut. Dies ist ein von den wichtigsten Architekturprinzipien für die Aufrechterhaltung von sauberem, fokussiertem Code, der sich nicht in unüberschaubare Komplexität auswächst.

So wird Code im Laufe der Zeit schlechter: Ein Dienst wird für einen klaren, spezifischen Zweck geschrieben. Die Logik ist sauber und fokussiert. Dann kommt eine neue Produkterforderung, und jemand fügt eine if-Anweisung hinzu. Einige Jahre und mehrere weitere Anforderungen später ist dieser Dienst mit bedingten Überprüfungen für verschiedene Szenarien übersät. Der Dienst ist geworden:

  • Kompliziert und schwer zu lesen

  • Schwer verständlich und schwer nachvollziehbar

  • Anfälliger für Fehler

  • Verletzung der Einzelverantwortung (Bearbeitung zu vieler verschiedener Fälle)

  • Nahezu unmöglich, gründlich zu testen

Der Dienst hat seine Grenzen überschritten in Bezug auf Verantwortlichkeiten und Logik.

Eine Lösung: Fabrikmuster mit spezialisierten Diensten
Verwenden Sie das Fabrikmuster, um Entscheidungen am Eingangspunkt zu treffen, und übergeben Sie dann an spezialisierte Dienste, die ihre spezifische Logik ohne Bedingungsanweisungen behandeln.

Beispiel aus unserer Codebasis:
Der BillingPortalServiceFactory bestimmt, ob die Abrechnung für eine Organisation, ein Team oder einen einzelnen Benutzer erfolgt und gibt dann den entsprechenden Dienst zurück:

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

Jeder Dienst behandelt dann seine spezifische Logik, ohne "bin ich eine Organisation oder ein Team?" zu überprüfen:

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

Warum das wichtig ist

  • Dienste bleiben fokussiert

    • Jeder Dienst hat eine Verantwortung und muss nichts über andere Kontexte wissen. Der OrganizationBillingPortalService enthält keine if-Anweisungen, die if (isTeam) oder if (isUser) überprüfen. Es weiß nur, wie man mit Organisationen umgehen kann.

  • Änderungen sind isoliert

    • Wenn Sie die Organisation Abrechnungslogik ändern müssen, berühren Sie nur OrganizationBillingPortalService. Sie riskieren nicht, die Team- oder Benutzerabrechnung zu brechen. Sie müssen keine verschachtelten Bedingungsanweisungen nachverfolgen, um herauszufinden, welchen Weg Ihr Code nimmt.

  • Das Testen ist unkompliziert

    • Testen Sie jeden Dienst unabhängig mit seinen spezifischen Szenarien. Es besteht keine Notwendigkeit, jede Kombination von Bedingungsanweisungen über verschiedene Kontexte hinweg zu testen.

  • Neue Anforderungen verschmutzen nicht den vorhandenen Code

    • z.B. Wenn Sie Enterprise-Abrechnung mit verschiedenen Regeln hinzufügen müssen, erstellen Sie EnterpriseBillingPortalService. Die Fabrik erhält eine weitere Bedingung, aber bestehende Dienste bleiben unberührt und fokussiert.

Wie man es erreicht

  • Verlagern Sie Bedingungsanweisungen zu Controllern, Fabriken oder Routinglogik. Lassen Sie diese Eingabepunkte die Entscheidung darüber treffen, welchen Dienst zu verwenden ist.

  • Halten Sie Dienste rein und fokussiert auf eine einzelne Verantwortung. Wenn ein Dienst überprüfen muss "welcher Typ bin ich?", benötigen Sie wahrscheinlich mehrere Dienste.

  • Bevorzugen Sie Polymorphismus gegenüber Bedingungsanweisungen

    • Schnittstellen definieren den Vertrag. Konkrete Implementierungen liefern die Details.

  • Beobachten Sie die Ansammlung von if-Anweisungen

    • Während der Code-Review, wenn Sie sehen, dass ein Dienst Bedingungsanweisungen für verschiedene Szenarien erhält, ist das ein Signal, um in spezialisierte Dienste zu refaktorieren.

API-Design: Dünne Controller und HTTP-Abstraktion

  • Controller sind dünne Schichten, die nur HTTP-Belange behandeln.

    • Sie nehmen Anfragen entgegen, verarbeiten sie und transformieren Daten in DTOs, die an die Kernanwendungslogik übergeben werden. Zukünftig sollte keine Anwendungs- oder Kernlogik in API-Routen oder tRPC-Handlern zu finden sein.

  • Wir müssen die HTTP-Technologie von unserer Anwendung trennen.

    • Die Art und Weise, wie wir Daten zwischen Client und Server übertragen (sei es REST, tRPC, etc.), sollte nicht beeinflussen, wie unsere Kernanwendung funktioniert. HTTP ist ein Übermittlungsmechanismus und kein architektonischer Treiber.

Controller-Verantwortungen (und NUR diese):

  • Eingehende Anfragen empfangen und validieren

  • Daten aus Anfrageparametern, Body, Headern extrahieren

  • Anforderungsdaten in DTOs transformieren

  • Geeignete Anwendungsdienste mit diesen DTOs aufrufen

  • Antworten der Anwendungsdienste in Antwort-DTOs transformieren

  • HTTP-Antworten mit den richtigen Statuscodes zurückgeben

Controller sollten NICHT:

  • Geschäftslogik oder Domänenregeln enthalten

  • Direkt auf Datenbanken oder externe Dienste zugreifen

  • Komplexe Datenumwandlungen oder Berechnungen durchführen

  • Entscheidungen darüber treffen, was die Anwendung tun sollte

  • Über Implementierungsdetails der Domäne Bescheid wissen

Beispiel für ein dünnes Controller-Muster:


API-Versionierung und Breaking Changes

Keine Breaking Changes. Das ist entscheidend. Sobald ein API-Endpunkt öffentlich ist, muss er stabil bleiben. Breaking Changes zerstören das Entwicklervetrauen und schaffen Integrations-Albträume für unsere Nutzer.

Strategien zur Vermeidung von Breaking Changes:

  • Fügen Sie neue Felder immer als optional hinzu

  • Verwenden Sie API-Versionierung, wenn Sie bestehendes Verhalten ändern müssen

  • Veraltete alte Endpunkte schonend mit klaren Migrationspfaden

  • Erhalten Sie die Rückwärtskompatibilität für mindestens zwei Hauptversionen aufrecht

Wenn Sie Breaking Changes vornehmen müssen:

  • Erstellen Sie eine neue API-Version mit dateispezifischer Versionierung in API v2 (vielleicht schauen wir uns auch die kürzlich eingeführte benannte Versionsnummerierung von Stripe an)

  • Führen Sie während der Übergangsphase beide Versionen gleichzeitig aus (wir tun dies bereits in API v2)

  • Stellen Sie bei Möglichkeit automatisierte Migrationstools bereit

  • Geben Sie den Benutzern genügend Zeit zum Migrieren (mindestens 6 Monate für öffentliche APIs)

  • Dokumentieren Sie genau, was sich geändert hat und warum

Performance und Algorithmuskomplexität

Wir entwickeln für große Organisationen und Teams. Was bei 10 Nutzern oder 50 Datensätzen gut funktioniert, kann unter dem Gewicht des Unternehmensskalar zusammenbrechen. Performance ist nichts, was wir später optimieren. Es ist etwas, das wir von Anfang an korrekt aufbauen.

Denken Sie von Anfang an über Skalierbarkeit nach

Beim Erstellen von Funktionen immer fragen: "Wie verhält es sich mit 1.000 Benutzern? 10.000 Datensätzen? 100.000 Operationen?" Der Unterschied zwischen O(n) und O(n²)-Algorithmen mag in der Entwicklung unmerklich sein, kann aber in der Produktion katastrophal sein.

Häufige O(n²)-Muster zu vermeiden:

  • Verschachtelte Array-Iterationen (.map innerhalb von .map, .forEach innerhalb von .forEach)

  • Array-Methoden wie .some, .find oder .filter in Schleifen oder Rückrufen

  • Jedes Element gegen jedes andere Element ohne Optimierung prüfen

  • Chained Filters oder verschachteltes Mappen über große Listen

Beispiel aus der Praxis: Für 100 verfügbare Slots und 50 belegte Zeiten führt ein O(n²)-Algorithmus 5.000 Prüfungen durch. Skalieren Sie das auf 500 Slots und 200 belegte Zeiten und Sie führen 100.000 Operationen durch. Das ist eine 20-fache Erhöhung der Rechenlast nur für eine 5-fache Erhöhung der Daten.

Wählen Sie die richtigen Datenstrukturen und Algorithmen

Die meisten Leistungsprobleme werden durch die Auswahl besserer Datenstrukturen und Algorithmen gelöst:

  • Sortieren + Frühausstieg: Sortieren Sie Ihre Daten einmal und brechen Sie Schleifen ab, wenn Sie wissen, dass verbleibende Elemente nicht übereinstimmen

  • Binäre Suche: Verwenden Sie die binäre Suche für Suchen in sortierten Arrays anstelle von linearen Scans

  • Zweipunkt-Techniken: Zum Zusammenführen oder Überschneiden sortierter Sequenzen, gehen Sie durch beide mit Zeigern anstelle von verschachtelten Schleifen

  • Hash-Maps/Sets: Verwenden Sie Objekte oder Sets für O(1)-Suche anstelle von .find oder .includes auf Arrays

  • Intervall-Bäume: Für Planung, Verfügbarkeit und Bereichsabfragen, verwenden Sie ordnungsgemäße Baumstrukturen anstelle von brutalen Vergleichen

Beispiel Umwandlung:

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

Automatisierte Leistungstests

Wir werden mehrere Abwehrmechanismen gegen Leistungsregressionen umsetzen:

Linting-Regeln die markieren:

  • Funktionen mit verschachtelten Schleifen oder verschachtelten Array-Methoden

  • Mehrfach verschachtelte .some, .find oder .filter-Aufrufe

  • Rekursion ohne Speicherung

  • Bekannte Anti-Muster für unser Gebiet (Planung, Verfügbarkeitsprüfungen usw.)

Performance-Benchmarks in CI die:

  • Kritische Algorithmen auf realistischen, großskaligen Daten ausführen

  • Ausführungszeiten gegen Basislinie bei jedem PR vergleichen

  • Zusammenführungen blockieren, die Leistungsregressionen einführen

  • Mit Unternehmensmaßstab-Daten testen (tausende Benutzer, zehntausende Datensätze)

Produktionsüberwachung die:

  • Ausführungszeit für kritische Pfade verfolgt

  • Alarmiert, wenn Algorithmen langsamer werden, wenn Daten wachsen

  • Erkennt Regressionen, bevor Benutzer sie bemerken

  • Bietet reale Leistungsdaten zur Optimierungsvorbereitung

Performance ist ein Feature

Performance ist nicht optional. Es ist nichts, was wir "später angehen". Für Unternehmenskunden, die über große Teams buchen, bedeuten langsame Antworten verlorene Produktivität und frustrierte Benutzer (unsere Erfahrung mit einigen größeren Unternehmenskunden ist ein Zeugnis dafür).

Jeder Ingenieur sollte:

  • Ihr Code profilieren, bevor Sie optimieren, aber von Anfang an über die Komplexität nachdenken

  • Mit realistischen, großskaligen Daten testen (nicht nur 5 Testdatensätze). Wir haben bereits Seed-Skripte gebaut. Wir müssen sie wahrscheinlich erweitern.

  • Von Anfang an effiziente Algorithmen und Datenstrukturen wählen

  • Bei Code-Review auf verschachtelte Iterationen achten

  • Jeden Algorithmus in Frage stellen, der mit dem Produkt von zwei Variablen skaliert

Die NP-schwere Realität der Planung

Planungsprobleme sind von Natur aus NP-schwer. Das bedeutet, dass mit zunehmender Anzahl von Einschränkungen, Teilnehmern oder Zeitschlitzen die Rechenkomplexität exponentiell ansteigen kann. Die meisten optimalen Planungsalgorithmen haben in der worst-case-Szenario exponentielle Zeitkomplexität, die Algorithmuswahl entscheidend macht.

Realweltimplikationen:

  • Die optimale Meetingzeit für 10 Personen über 3 Zeitzonen mit individuellen Verfügbarkeitsbeschränkungen zu finden, ist rechnerisch teuer

  • Die Hinzufügung von Konflikterkennung, Puffern und einer Vielzahl anderer Optionen verstärkt das Problem

  • Schlechte Algorithmuswahl, die bei kleinen Teams gut funktionierte, wird für große Organisationen völlig unbrauchbar

  • Was Millisekunden bei 5 Benutzern dauert, könnte viele Sekunden bei Organisationen dauern

Strategien zum Management der NP-schweren Komplexität:

  • Verwenden Sie Approximationsalgorithmen, die "gut genug"-Lösungen schnell finden, anstatt perfekte Lösungen langsam

  • Implementieren Sie aggressives Caching von berechneten Zeitplänen und Verfügbarkeiten

  • Berechnen Sie häufige Szenarien während der Nebenzeiten vor

  • Teilen Sie große Planungsprobleme in kleinere, handlichere Stücke

  • Setzen Sie vernünftige Zeitlimits und greifen Sie bei Bedarf auf einfachere Algorithmen zurück

Aus diesem Grund ist Performance nicht nur ein nettes Extra in Planungssoftware. Es ist die Grundlage, die bestimmt, ob Ihr System auf Unternehmensbedürfnisse skalieren kann oder unter realen Nutzungsmustern zusammenbricht.

Anforderungen an die Codeabdeckung

  • Globales Abdeckungs-Tracking

    • Wir verfolgen die Gesamtcodeabdeckung als wichtigen Metrik, die sich im Laufe der Zeit verbessert. Das gibt uns Einblicke in unsere Test-Reife und hilft, Bereiche zu identifizieren, die Aufmerksamkeit erfordern. Der globale Abdeckungsprozentsatz wird auf unseren Dashboards im Vordergrund angezeigt.

  • 80%+ Abdeckung für neuen Code

    • Jeder PR muss eine nahezu 80%+ Abdeckung für den Code haben, den er einführt oder ändert. Dies wird automatisch in unserer CI-Pipeline erzwungen. Wenn Sie 50 Zeilen neuen Code hinzufügen, müssen diese 50 Zeilen durch Tests abgedeckt sein. Wenn Sie eine vorhandene Funktion ändern, müssen Ihre Änderungen getestet werden. Dies ist eine Gesamtcodeabdeckung. Die Abdeckung von Unittests muss nahezu 100% erreichen, insbesondere mit der Möglichkeit, KI zu verwenden, um dabei zu helfen, diese zu generieren.

Das Argument "Abdeckung ist nicht die ganze Geschichte" entkräften: Ja, wir wissen, dass Abdeckung keine perfekten Tests garantiert. Wir wissen, dass Sie sinnlose Tests schreiben können, die jede Zeile berühren, aber nichts Bedeutendes testen. Wir wissen, dass Abdeckung nur einer von vielen Metriken ist. Aber es ist sicherlich besser, einen hohen Prozentsatz anzustreben, als keine Ahnung zu haben, wo Sie sich befinden.

Erfolg messen

  • "Velocity" (dies von Scrum gestohlen, obwohl wir Scrum nicht verwenden werden)

    • Stetiges Wachstum in monatlichen Statistiken (Funktionen, Verbesserungen, Refaktorierungen)

  • Qualität

    • Reduzieren Sie den Arbeitsaufwand in PRs, der für Korrekturen aufgewendet wird, von derzeit 35% auf 20% oder weniger bis Ende 2026 (berechnet basierend auf Dateieingaben und Änderungen/Löschungen)

  • Architekturale Gesundheit

    • Metriken zur Muster-Adhärenz, technologisches Koppeln, Grenzverletzungen

  • Überprüfungseffizienz

    • Kleinere PRs, schnellere Überprüfungen, weniger Feedback-Runden

  • Anwendungs- und API-Verfügbarkeit

    • Wie nah sind wir an 99,99%?

Beginnen Sie noch heute kostenlos mit Cal.com!

Erleben Sie nahtlose Planung und Produktivität ohne versteckte Gebühren. Melden Sie sich in Sekunden an und beginnen Sie noch heute, Ihre Planung zu vereinfachen, ganz ohne Kreditkarte!