Blog

Inżynieria w 2026 roku i później

Budujemy infrastrukturę, która prawie nigdy nie może zawodzić. Żeby to osiągnąć, działamy szybko, jednocześnie dostarczając świetne oprogramowanie bez skrótów i kompromisów. Ten dokument opisuje standardy inżynieryjne, które będą nas prowadzić przez 2026 rok i później.

Keith WilliamsOct 14, 2025

Struktura zespołu

Nasza organizacja inżynieryjna składa się z pięciu głównych zespołów, z których każdy ma inne obowiązki:

  • Foundation Team: Skupia się na ustanawianiu i utrzymywaniu standardów kodowania oraz wzorców architektonicznych. Ten zespół współpracuje z innymi zespołami, żeby wypracować najlepsze praktyki w całej organizacji.

  • Consumer, Enterprise i Platform Teams: Zespoły produktowe, które szybko dostarczają funkcje, jednocześnie utrzymując standardy jakości określone w tym dokumencie. Te zespoły pokazują, że szybkość i jakość nie wykluczają się nawzajem.

  • Community Team: Odpowiada za szybkie przeglądanie PR-ów od społeczności open source, przekazywanie feedbacku i prowadzenie tej pracy aż do mergu. Ten zespół dba o to, aby nasi współtwórcy open source mieli świetne doświadczenie, a ich wkład spełniał nasze standardy jakości. Utrzymywanie silnej kultury open-source nadal kształtuje to, jak Cal.com podchodzi do współpracy inżynieryjnej i rozwoju produktu.

Nasze dotychczasowe wyniki

Dane mówią same za siebie. W ciągu ostatniego półtora roku całkowicie zmieniliśmy sposób, w jaki budujemy oprogramowanie:

Screenshot 2025-10-01 at 9.40.36 PM.png

Mniej więcej podwoiliśmy naszą produktywność inżynieryjną, jednocześnie poprawiając jakość. Jeszcze bardziej imponująca jest zmiana tego, co budujemy:

Screenshot 2025-10-01 at 9.41.51 PM.png

Skutecznie przekierowaliśmy około 20% wysiłku inżynieryjnego z poprawek na funkcje, usprawnienia wydajności, refaktoryzacje i prace porządkowe. Ta zmiana pokazuje, że inwestowanie w jakość i architekturę nie spowalnia. Przyspiesza.

Fundament Cal.com umożliwia doskonałość dla coss.com

  • Cal.com to stabilny, dochodowy biznes, który będziemy dalej rozwijać.

    • Ten sukces daje nam wyjątkową przewagę, kiedy budujemy coss.com. W przeciwieństwie do początków Cal.com, kiedy musieliśmy działać szybko, żeby osiągnąć product-market fit i zbudować zrównoważony biznes, coss.com startuje z pozycji siły.
  • Nie musimy się spieszyć z coss.com.

    • Stabilność Cal.com oznacza, że możemy pozwolić sobie zbudować coss.com we właściwy sposób od pierwszego dnia. Mamy luksus wdrażania tych standardów inżynieryjnych bez presji natychmiastowych wymagań rynku czy ograniczeń finansowania. To zupełnie inny punkt startowy.
  • Ta „powolność” to inwestycja, nie koszt.

    • Tak, trzymanie się tych standardów na początku może wydawać się wolniejsze, a nawet frustrujące dla niektórych inżynierów. Pisanie DTO zajmuje więcej czasu niż bezpośrednie przekazywanie typów bazy danych do frontendu. Tworzenie porządnych abstrakcji i dependency injection wymaga więcej wstępnego projektowania. Utrzymywanie ponad 80% pokrycia testami dla nowego kodu wymaga dyscypliny. Ale ta pozorna powolność jest tymczasowa, a zysk jest wykładniczy.

Zobacz skumulowane korzyści...

  • Kod zaprojektowany poprawnie od początku nie wymaga później ogromnych refaktoryzacji

  • Wysokie pokrycie testami zapobiega błędom, które w innym przypadku pochłonęłyby tygodnie debugowania i hotfixów (zobacz 2023 do połowy 2024)

  • Dobre abstrakcje sprawiają, że z czasem dodawanie nowych funkcji staje się znacznie szybsze

  • Czyste granice i DTO zapobiegają erozji architektury, która ostatecznie wymusza pełne przepisywanie kodu

**Cal.com trajektoria pokazuje, co się dzieje, gdy optymalizujesz pod kątem natychmiastowej szybkości. Wysoka początkowa prędkość, która stopniowo spada wraz z narastaniem długu technicznego, skróty architektoniczne tworzące wąskie gardła i coraz więcej czasu spędzanego na naprawianiu problemów zamiast budowaniu funkcji (zobacz poprzedni wykres, gdzie 55-60% wysiłku inżynieryjnego szło na poprawki).

Trajektoria coss.com będzie korzystać z siły budowania dobrze od pierwszego dnia. Trochę wolniejsza początkowa prędkość przy ustanawianiu właściwych wzorców, a potem wykładnicze przyspieszenie, gdy te wzorce zaczną przynosić efekty i umożliwią szybszy rozwój z większą pewnością.

Kluczowe zasady

1. Brak odkładania jakości

  • Będziemy minimalizować „zrobię to w follow-upowym PR-ze” przy małych refaktoryzacjach.

    • Follow-upowe PR-y z drobnymi usprawnieniami prawie nigdy się nie materializują. Zamiast tego gromadzą się jako dług techniczny, który obciąża nas miesiące albo lata później. Jeśli małą refaktoryzację da się zrobić teraz, zrób ją teraz. Follow-upy powinny być zarezerwowane na duże zmiany, które naprawdę uzasadniają osobne PR-y, albo na wyjątkowe, pilne przypadki.

2. Wysokie standardy w code review

  • Nie przepuszczaj PR-ów z masą drobiazgów tylko po to, żeby nie wyjść na „złą osobę”.

    • Dokładnie tak kod bazy z czasem robi się niedbały. Code review nie polega na byciu miłym. Polega na utrzymywaniu standardów jakości, których wymaga nasza infrastruktura. Każdy drobiazg ma znaczenie. Każde naruszenie wzorca ma znaczenie. Zajmijcie się tym przed mergem, nie po nim.

3. Wzajemnie pchajmy się do robienia właściwych rzeczy

  • Rozliczamy się nawzajem z jakości

    • Pójście na skróty może w danej chwili wydawać się szybsze, ale później tworzy problemy, które spowalniają wszystkich. Kiedy widzisz, że ktoś z zespołu zaraz merguje PR z oczywistymi problemami, odezwij się. Kiedy ktoś proponuje szybki hack zamiast właściwego rozwiązania, zaprotestuj. Kiedy kusi cię, żeby pominąć testy albo zignorować wzorce architektoniczne, spodziewaj się, że zespół cię zakwestionuje.
  • To nie chodzi o bycie trudnym albo spowalnianie ludzi

    • Chodzi o wspólną odpowiedzialność za nasz codebase i naszą reputację. Każdy skrót, który ktoś zrobi, staje się problemem wszystkich. Każdy skrót dziś oznacza jutro więcej sesji debugowania, więcej hotfixów i więcej sfrustrowanych klientów.
  • Sprawmy, żeby kwestionowanie złych decyzji było normalne, ale z szacunkiem

    • Jeśli ktoś mówi „na razie po prostu hard-code'ujmy to”, oczekiwana odpowiedź powinna brzmieć: „co byłoby potrzebne, żeby zrobić to od razu we właściwy sposób?”. Jeśli ktoś chce zatwierdzić nieprzetestowany kod, zespół powinien się temu sprzeciwić. Jeśli ktoś proponuje kopiowanie i wklejanie zamiast stworzenia porządnej abstrakcji, zwróć uwagę — z szacunkiem.
  • Budujemy coś, co prawie nigdy nie może zawieść

    • Taki poziom niezawodności nie bierze się przypadkiem. Pojawia się wtedy, gdy każdy inżynier czuje odpowiedzialność za jakość, nie tylko za własny kod, ale za cały system. Albo wygrywamy jako zespół, albo jako zespół przegrywamy.

4. Stawiajmy na prostotę

  • Priorytetem jest jasność, nie spryt

    • Celem jest kod, który łatwo i szybko przeczytać oraz zrozumieć, a nie elegancka złożoność. Proste systemy zmniejszają obciążenie poznawcze każdego inżyniera.
  • Zadaj sobie właściwe pytania

    • Czy naprawdę rozwiązuję ten problem, który mam przed sobą?

    • Czy nie myślę za dużo o możliwych przyszłych przypadkach użycia?

    • Czy rozważyłem przynajmniej 1 inną alternatywę rozwiązania tego? Jak się ona ma do obecnej?

  • Proste nie znaczy pozbawione funkcji

    • Sam fakt, że naszym celem jest tworzenie prostych systemów, nie znaczy, że mają sprawiać wrażenie anemicznych i pozbawionych oczywistej funkcjonalności.

5. Automatyzujmy wszystko

  • Korzystajmy z AI

    • Generuj 80% boilerplate'u i niekrytycznego kodu z użyciem AI, żebyśmy mogli skupić się wyłącznie na złożonej logice biznesowej i krytycznej architekturze. Zespoły wdrażające nowoczesne workflowy wspierane przez AI coraz częściej polegają na narzędziach takich jak best MCP servers, żeby poprawić produktywność developerów i orkiestrację.

    • Budujmy alerty bez szumu i inteligentną obsługę błędów.

    • Ręczne testowanie coraz bardziej odchodzi do przeszłości. AI może szybko i inteligentnie budować dla nas ogromne zestawy testów.

  • Nasze CI to final boss

    • Wszystko w tym dokumencie standardów jest sprawdzane, zanim kod zostanie zmergowany w PR-ach

    • Żadne niespodzianki nie trafiają do main

    • Sprawdzenia są szybkie i użyteczne

Standardy architektoniczne

Przechodzimy na ścisły model architektoniczny oparty na Vertical Slice Architecture i Domain-Driven Design (DDD). Poniższe wzorce i zasady będą rygorystycznie egzekwowane podczas review PR-ów i za pomocą lintingu.

Vertical Slice Architecture: packages/features

Nasz codebase jest zorganizowany według domen, a nie według warstwy technicznej. Katalog packages/features jest sercem tego podejścia architektonicznego. Każdy folder w środku reprezentuje pełny pionowy wycinek aplikacji, sterowany domeną, której dotyczy.

Struktura:

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

Każdy folder feature to samodzielny pionowy wycinek, który zawiera wszystko potrzebne dla tej domeny:

  • Logika domenowa: Główne reguły biznesowe i encje specyficzne dla tej funkcji

  • Usługi aplikacyjne: Orkiestracja przypadków użycia dla tej domeny

  • Repozytoria: Dostęp do danych specyficzny dla potrzeb tej funkcji

  • DTO: Obiekty transferu danych do przekraczania granic

  • Komponenty UI: Komponenty frontendowe związane z tą funkcją, tam gdzie ma to zastosowanie

  • Testy: Testy jednostkowe, integracyjne i e2e dla tej funkcji

Dlaczego pionowe wycinki mają znaczenie

Tradycyjna architektura warstwowa organizuje kod według zagadnień technicznych:

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

To powoduje kilka problemów:

  • Zmiany w jednej funkcji wymagają dotykania plików rozrzuconych po wielu katalogach

  • Trudno zrozumieć, co robi dana funkcja, ponieważ jej kod jest rozdrobniony

  • Zespoły wchodzą sobie w drogę, pracując nad różnymi funkcjami

  • Nie możesz łatwo wydzielić ani wycofać funkcji

Architektura z pionowymi wycinkami porządkuje wszystko według domeny:

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

To rozwiązuje te problemy:

  • Wszystko związane z dostępnością znajduje się w packages/features/availability

  • Możesz zrozumieć całą funkcję dostępności, zaglądając do jednego katalogu

  • Zespoły mogą pracować nad różnymi funkcjami bez konfliktów (jeśli zespół inżynieryjny Cal.com urośnie, ale na pewno w coss.com będziemy powierzać zespołom duże pakiety)

  • Funkcje są luźno powiązane i mogą rozwijać się niezależnie

Wskazówki dotyczące organizacji funkcji

Teoretycznie każda funkcja może być wdrażana niezależnie. Nawet jeśli nie będziemy ich faktycznie wdrażać osobno, taka organizacja zmusza nas do utrzymywania jasnych zależności i minimalnego sprzężenia. To jest założenie i sukces mikroserwisów, choć jeszcze nie będziemy wdrażać mikroserwisów.

Funkcje komunikują się przez dobrze zdefiniowane interfejsy. Jeśli bookings potrzebuje danych o availability, importuje je z @calcom/features/availability przez eksportowane interfejsy, a nie zagląda do wewnętrznych szczegółów implementacji.

Wspólny kod trafia we właściwe miejsca:

  • Narzędzia niezależne od domeny i elementy przekrojowe (auth, logging): packages/lib

  • Wspólne prymitywy UI: packages/ui (a wkrótce ui coss.com)

Granice domen są egzekwowane automatycznie. Zbudujemy linting, który uniemożliwi zaglądanie do wnętrza funkcji tam, gdzie nie powinno się tego robić. Jeśli packages/features/bookings spróbuje zaimportować coś z packages/features/availability/services/internal, linter to zablokuje. Wszystkie zależności między funkcjami muszą przechodzić przez publiczne API danej funkcji.

good_architecture_diagram.pngbad_architecture_diagram.png

Nowe funkcje zaczynają się jako pionowe wycinki. Gdy budujesz coś nowego, utwórz nowy folder w packages/features z kompletnym pionowym wycinkiem. Dzięki temu od razu widać, co budujesz, i wszystko pozostaje uporządkowane od pierwszego dnia.

Korzyści

  • Łatwość odnajdywania

    • Szukasz logiki rezerwacji? Wszystko jest w packages/features/bookings. Nie musisz przekopywać się przez kontrolery, serwisy, repozytoria i narzędzia porozrzucane po całej bazie kodu.
  • Łatwiejsze testowanie

    • Testuj całą funkcję jako jedną całość. Masz wszystkie elementy w jednym miejscu, więc testy integracyjne są naturalne i proste.
  • Jaśniejsze zależności

    • Kiedy widzisz import { getAvailability } from '@calcom/features/availability', dokładnie wiesz, od której funkcji zależysz. Gdy zależności robią się zbyt złożone, od razu to widać i można to naprawić.

Wzorzec Repository i Dependency Injection

Wybory technologiczne nie mogą przenikać przez całą aplikację. Problem z Prisma pokazuje to idealnie. Obecnie mamy odwołania do Prisma rozsiane po setkach plików. To tworzy ogromne sprzężenie i sprawia, że zmiany technologiczne są absurdalnie kosztowne. Odczuwamy to teraz przy aktualizacji Prisma do v6.16. To, co powinno być tylko lokalnym refaktorem za osłoniętymi repozytoriami, zamieniło się w krętą, niemal niekończącą się pogoń za problemami w wielu aplikacjach.

Standard od teraz:

  • Cały dostęp do bazy danych musi przechodzić przez klasy Repository. Mamy już całkiem dobry start.

  • Repozytoria to jedyny kod, który wie o Prisma (albo jakimkolwiek innym ORM). Nie powinno się w nich trzymać żadnej logiki.

  • Repozytoria są wstrzykiwane przez kontenery Dependency Injection

  • Jeśli kiedykolwiek przejdziemy z Prisma na Drizzle albo inny ORM, jedyne wymagane zmiany to:

    • Implementacje repozytoriów

    • Okablowanie kontenera DI dla nowych repozytoriów

    • Nic innego w bazie kodu nie powinno tego obchodzić ani wymagać zmian

To nie jest teoria. Tak budujemy systemy, które da się utrzymywać.

Obiekty transferu danych (DTO)

Typy bazy danych nie powinny przeciekać do frontendu. To stało się popularnym skrótem w naszym stosie technologicznym, ale to code smell, który tworzy wiele problemów.

  • Sprzężenie technologiczne (typy Prisma trafiają do komponentów React)

  • Ryzyko bezpieczeństwa (przypadkowy wyciek wrażliwych pól)

  • Kruche kontrakty między serwerem a klientem (to szczególnie problematyczne, gdy budujemy dużo więcej API)

  • Brak możliwości niezależnego rozwijania schematu bazy danych

  • Wszystkie konwersje DTO przez Zod, nawet dla odpowiedzi API, żeby mieć pewność, że wszystkie dane są walidowane przed wysłaniem do użytkownika. Lepiej się wyłożyć, niż zwrócić coś błędnego.

Standard od teraz:
Twórz jawne DTO na każdej granicy architektonicznej.

  1. Warstwa danych → Warstwa aplikacji → API: Przekształcaj modele bazy danych w DTO warstwy aplikacji, a potem przekształcaj DTO aplikacji w DTO specyficzne dla API

  2. API → Warstwa aplikacji → Warstwa danych: Przekształcaj DTO API przez warstwę aplikacji i dalej w DTO specyficzne dla danych

Tak, to wymaga więcej kodu. Tak, warto. Jawne granice zapobiegają architektonicznej erozji, która tworzy długoterminowy koszmar utrzymania.

Wzorce domain-driven design

Poniższe wzorce muszą być używane poprawnie i konsekwentnie:

  • Application Services

    • Orkiestrują przypadki użycia, koordynują działanie między usługami domenowymi i repozytoriami
  • Domain Services

    • Zawierają logikę biznesową, która naturalnie nie należy do pojedynczej encji
  • Repositories

    • Abstrahują dostęp do danych, izolują wybory technologiczne
  • Dependency Injection

    • Umożliwia luźne powiązanie, ułatwia testowanie, izoluje odpowiedzialności
  • Caching Proxies

    • Owijają repozytoria lub usługi, aby transparentnie dodać zachowanie cache'owania

    • Oczywiście nie jest to jedyny sposób na cache'owanie, ale to dobry punkt wyjścia

  • Decorators

    • Dodają elementy przekrojowe (logging, metryki itd.) bez zanieczyszczania logiki domenowej

Spójność bazy kodu

Nasze bazy kodu powinny sprawiać wrażenie, jakby napisała je jedna osoba. Taki poziom spójności wymaga ścisłego trzymania się ustalonych wzorców, kompleksowych reguł lintingu egzekwujących standardy architektoniczne, code review odrzucających łamanie wzorców oraz pomocy AI reviewerów kodu.

Przenieś warunki do punktu wejścia aplikacji

Instrukcje if powinny znajdować się w punkcie wejścia, a nie być rozrzucone po całych twoich usługach. To jedna z najważniejszych zasad architektonicznych pozwalających utrzymać czysty, skupiony kod, który nie przechodzi w nieutrzymywalną złożoność.

Tak z czasem degraduje się kod: Usługa jest napisana do jasnego, konkretnego celu. Logika jest czysta i skupiona. Potem pojawia się nowy wymóg produktowy i ktoś dodaje instrukcję if. Kilka lat i jeszcze kilka wymagań później ta usługa jest usiana warunkami dla różnych scenariuszy. Usługa stała się:

  • Złożona i trudna do czytania

  • Trudna do zrozumienia i przeanalizowania

  • Bardziej podatna na błędy

  • Łamiąca zasadę pojedynczej odpowiedzialności (obsługuje zbyt wiele różnych przypadków)

  • Prawie niemożliwa do dokładnego przetestowania

Usługa przekroczyła swoje granice jeśli chodzi o odpowiedzialności i logikę.

Rozwiązanie: wzorzec fabryki ze specjalistycznymi usługami
Użyj wzorca fabryki, żeby podejmować decyzje w punkcie wejścia, a potem przekazuj dalej do wyspecjalizowanych usług, które obsługują swoją konkretną logikę bez warunków.

Przykład z naszej bazy kodu:
BillingPortalServiceFactory określa, czy rozliczenia dotyczą organization, teamu czy pojedynczego użytkownika, a potem zwraca odpowiednią usługę:

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

Każda usługa obsługuje potem swoją konkretną logikę bez potrzeby sprawdzania „czy jestem org czy teamem?”:

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

Dlaczego to ma znaczenie

  • Usługi pozostają skupione

    • Każda usługa ma jedną odpowiedzialność i nie musi wiedzieć o innych kontekstach. OrganizationBillingPortalService nie zawiera instrukcji if sprawdzających if (isTeam) albo if (isUser). Wie tylko, jak obsługiwać organization.
  • Zmiany są odizolowane

    • Gdy musisz zmodyfikować logikę rozliczeń organizacji, dotykasz tylko OrganizationBillingPortalService. Nie ryzykujesz zepsucia rozliczeń zespołu ani użytkownika. Nie musisz śledzić zagnieżdżonych warunków, żeby ustalić, którą ścieżką pójdzie twój kod.
  • Testowanie jest proste

    • Testujesz każdy serwis niezależnie, z jego własnymi scenariuszami. Nie ma potrzeby testować każdej kombinacji warunków w różnych kontekstach.
  • Nowe wymagania nie zaśmiecają istniejącego kodu

    • Np. gdy musisz dodać rozliczenia enterprise z innymi zasadami, tworzysz EnterpriseBillingPortalService. Fabryka dostaje jeszcze jeden warunek, ale istniejące serwisy pozostają nietknięte i skupione.

Jak to osiągnąć

  • Wynoś warunki wyżej — do kontrolerów, fabryk albo logiki routingu. Niech te punkty wejścia decydują, którego serwisu użyć.

  • Trzymaj serwisy czyste i skupione na jednej odpowiedzialności. Jeśli serwis musi sprawdzać „kim jestem?”, prawdopodobnie potrzebujesz kilku serwisów.

  • Preferuj polimorfizm zamiast warunków

    • Interfejsy definiują kontrakt. Konkretne implementacje dostarczają szczegóły.
  • Uważaj na nagromadzenie instrukcji if

    • Podczas code review, jeśli widzisz, że w serwisie przybywa warunków dla różnych scenariuszy, to sygnał, że trzeba go rozbić na wyspecjalizowane serwisy.

Projekt API: Cienkie kontrolery i abstrakcja HTTP

  • Kontrolery to cienkie warstwy, które zajmują się tylko sprawami HTTP.

    • Przyjmują żądania, przetwarzają je i mapują dane do DTO, które trafiają do głównej logiki aplikacji. Idąc dalej, żadna logika aplikacji ani core logic nie powinna być widoczna w trasach API ani handlerach tRPC.
  • Musimy oddzielić technologię HTTP od naszej aplikacji.

    • Sposób, w jaki przekazujemy dane między klientem a serwerem (czy to REST, tRPC itd.), nie powinien wpływać na to, jak działa nasza główna aplikacja. HTTP to mechanizm dostarczania, a nie czynnik architektoniczny.

Obowiązki kontrolerów (i TYLKO te):

  • Przyjmuj i waliduj przychodzące żądania

  • Wyciągaj dane z parametrów żądania, body, nagłówków

  • Przekształcaj dane żądania w DTO

  • Wywołuj odpowiednie serwisy aplikacyjne z tymi DTO

  • Przekształcaj odpowiedzi serwisów aplikacyjnych w DTO odpowiedzi

  • Zwracaj odpowiedzi HTTP z właściwymi kodami statusu

Kontrolery NIE powinny:

  • Zawierać logiki biznesowej ani reguł domenowych

  • Bezpośrednio korzystać z baz danych ani usług zewnętrznych

  • Wykonywać złożonych transformacji danych ani obliczeń

  • Podejmować decyzji o tym, co powinna zrobić aplikacja

  • Znać szczegółów implementacyjnych domeny

Przykład wzorca cienkiego kontrolera:

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

Wersjonowanie API i zmiany niekompatybilne

Żadnych zmian niekompatybilnych. To krytyczne. Gdy endpoint API stanie się publiczny, musi pozostać stabilny. Zmiany niekompatybilne niszczą zaufanie deweloperów i tworzą koszmar integracyjny dla naszych użytkowników.

Strategie unikania zmian niekompatybilnych:

  • Zawsze dodawaj nowe pola jako opcjonalne

  • Używaj wersjonowania API, gdy musisz zmienić istniejące zachowanie

  • Starzejące się endpointy wycofuj stopniowo, z jasną ścieżką migracji

  • Utrzymuj wsteczną kompatybilność przez co najmniej dwie główne wersje

Gdy musisz wprowadzić zmiany niekompatybilne:

  • Utwórz nową wersję API, używając wersjonowania opartego na dacie w API v2 (być może przyjrzymy się też wersjonowaniu nazwami, które niedawno wprowadził Stripe)

  • Uruchamiaj obie wersje jednocześnie w okresie przejściowym (już to robimy w API v2)

  • Jeśli to możliwe, dostarczaj automatyczne narzędzia migracyjne

  • Daj użytkownikom dużo czasu na migrację (minimum 6 miesięcy dla publicznych API)

  • Udokumentuj dokładnie, co się zmieniło i dlaczego

Wydajność i złożoność algorytmiczna

Tworzymy dla dużych organizacji i teamów. To, co działa dobrze przy 10 użytkownikach albo 50 rekordach, może się załamać pod ciężarem skali enterprise. Wydajność nie jest czymś, co optymalizujemy później. To coś, co projektujemy poprawnie od samego początku.

Myśl o skali od pierwszego dnia

Budując funkcje, zawsze pytaj: "Jak to działa przy 1,000 użytkownikach? 10,000 rekordów? 100,000 operacji?" Różnica między algorytmami O(n) i O(n²) może być niezauważalna podczas developmentu, ale katastrofalna na produkcji.

Typowe wzorce O(n²), których trzeba unikać:

  • Zagnieżdżone iteracje po tablicach (.map wewnątrz .map, .forEach wewnątrz .forEach)

  • Metody tablicowe jak .some, .find albo .filter wewnątrz pętli lub callbacków

  • Sprawdzanie każdego elementu względem każdego innego bez optymalizacji

  • Łańcuchowe filtry albo zagnieżdżone mapowanie na dużych listach

Przykład z życia: Dla 100 dostępnych slotów i 50 zajętych okresów algorytm O(n²) wykonuje 5,000 sprawdzeń. Zwiększ to do 500 slotów i 200 zajętych okresów, a wykonujesz 100,000 operacji. To wzrost obciążenia obliczeniowego 20x przy zaledwie 5x większej ilości danych.

Wybierz odpowiednie struktury danych i algorytmy

Większość problemów z wydajnością rozwiązuje się, wybierając lepsze struktury danych i algorytmy:

  • Sortowanie + wczesne wyjście: Posortuj dane raz, a potem przerywaj pętle, gdy wiesz, że pozostałe elementy nie będą pasować

  • Wyszukiwanie binarne: Używaj wyszukiwania binarnego do lookupów w posortowanych tablicach zamiast liniowego przeszukiwania

  • Techniki dwóch wskaźników: Przy scalaniu lub przecinaniu posortowanych sekwencji przechodź przez obie za pomocą wskaźników zamiast zagnieżdżonych pętli

  • Mapy hashujące/zbiory: Używaj obiektów albo Setów do wyszukiwań O(1) zamiast .find albo .includes na tablicach

  • Drzewa przedziałowe: Do planowania, dostępności i zapytań zakresowych używaj właściwych struktur drzewiastych zamiast porównań brute-force

Przykład transformacji:

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

Automatyczne kontrole wydajności

Wdrożymy kilka warstw ochrony przed regresjami wydajności:

Reguły lintingu, które flagują:

  • Funkcje z zagnieżdżonymi pętlami albo zagnieżdżonymi metodami tablicowymi

  • Wielokrotne zagnieżdżone wywołania .some, .find albo .filter

  • Rekurencję bez memoizacji

  • Znane antywzorce dla naszej domeny (planowanie, sprawdzanie dostępności itd.)

Benchmarki wydajności w CI, które:

  • Uruchamiają krytyczne algorytmy na realistycznych, dużych danych

  • Porównują czasy wykonania z bazą przy każdym PR

  • Blokują merge'e wprowadzające regresje wydajności

  • Testują na danych enterprise-scale (tysiące użytkowników, dziesiątki tysięcy rekordów)

Monitoring produkcyjny, który:

  • Śledzi czas wykonania krytycznych ścieżek

  • Alarmuje, gdy algorytmy zwalniają wraz ze wzrostem danych

  • Wykrywa regresje, zanim zauważą je użytkownicy

  • Dostarcza rzeczywiste dane o wydajności, które pomagają w optymalizacjach

Wydajność to funkcja

Wydajność nie jest opcjonalna. To nie jest coś, co „ogarnie się później”. Dla klientów enterprise, którzy rezerwują terminy w dużych zespołach, wolne odpowiedzi oznaczają utratę produktywności i sfrustrowanych użytkowników (nasze doświadczenia z niektórymi większymi klientami enterprise mogą to potwierdzać).

Każdy inżynier powinien:

  • Zprofiluj swój kod przed optymalizacją, ale myśl o złożoności od samego początku

  • Testuj na realistycznych danych na dużą skalę (a nie tylko na 5 rekordach testowych). Mamy już gotowe skrypty seed. Prawdopodobnie trzeba je rozszerzyć.

  • Z góry wybieraj wydajne algorytmy i struktury danych

  • Zwracaj uwagę na zagnieżdżone iteracje podczas code review

  • Kwestionuj każdy algorytm, który skaluje się wraz z iloczynem dwóch zmiennych

Problem planowania jako NP-trudny

Problemy planowania są z definicji NP-trudne. Oznacza to, że wraz ze wzrostem liczby ograniczeń, uczestników albo slotów czasowych złożoność obliczeniowa może eksplodować wykładniczo. Większość optymalnych algorytmów planowania ma w najgorszym przypadku wykładniczą złożoność czasową, więc wybór algorytmu jest absolutnie kluczowy.

Konsekwencje w praktyce:

  • Znalezienie optymalnego terminu spotkania dla 10 osób w 3 strefach czasowych z indywidualnymi ograniczeniami dostępności jest kosztowne obliczeniowo

  • Dodanie wykrywania konfliktów, buforów i całej masy innych opcji tylko potęguje problem

  • Słabe wybory algorytmów, które sprawdzają się przy małych zespołach, stają się całkowicie bezużyteczne w dużych organizacjach

  • To, co dla 5 użytkowników zajmuje milisekundy, dla organizacji może trwać wiele sekund

Strategie radzenia sobie ze złożonością NP-trudną:

  • Używaj algorytmów przybliżonych, które szybko znajdują rozwiązania „wystarczająco dobre”, zamiast powoli szukać rozwiązań idealnych

  • Wdrażaj agresywne cache'owanie obliczonych harmonogramów i dostępności

  • Preobliczaj typowe scenariusze poza godzinami szczytu

  • Rozbijaj duże problemy planowania na mniejsze, łatwiejsze do ogarnięcia kawałki

  • Ustaw rozsądne limity czasu i w razie potrzeby przełączaj się na prostsze algorytmy

To dlatego wydajność w oprogramowaniu do planowania nie jest tylko miłym dodatkiem. To fundament, który decyduje o tym, czy Twój system da się skalować do potrzeb enterprise, czy rozsypie się pod realnymi wzorcami użycia.

Wymagania dotyczące pokrycia kodu

  • Globalne śledzenie pokrycia

    • Śledzimy ogólne pokrycie bazy kodu jako kluczową metrykę, która z czasem się poprawia. Daje nam to wgląd w dojrzałość testów i pomaga wskazać obszary, które wymagają uwagi. Globalny procent pokrycia jest wyraźnie pokazywany w naszych dashboardach.
  • Pokrycie nowego kodu na poziomie 80%+

    • Każdy PR musi mieć blisko 80%+ pokrycia testami dla kodu, który wprowadza lub modyfikuje. Jest to egzekwowane automatycznie w naszym pipeline CI. Jeśli dodasz 50 linii nowego kodu, te 50 linii musi być pokryte testami. Jeśli zmodyfikujesz istniejącą funkcję, Twoje zmiany muszą być przetestowane. To jest ogólne pokrycie testami. Pokrycie testami jednostkowymi musi być bliskie 100%, zwłaszcza że możemy wykorzystać AI, żeby pomóc je generować.

Odpowiedź na argument „pokrycie nie mówi całej prawdy”: Tak, wiemy, że pokrycie nie gwarantuje idealnych testów. Wiemy, że można pisać bezsensowne testy, które trafiają w każdą linię, ale niczego sensownego nie testują. Wiemy, że pokrycie to tylko jedna z wielu metryk. Ale i tak zdecydowanie lepiej celować w wysoki procent niż nie mieć pojęcia, gdzie się w ogóle jest.

Mierzenie sukcesu

  • „Velocity” (podkradamy to ze Scruma, chociaż nie będziemy używać Scruma)

    • Stały wzrost miesięcznych statystyk (funkcje, ulepszenia, refaktoryzacje)
  • Jakość

    • Zmniejszyć udział wysiłku PR poświęcanego na poprawki z obecnych 35% do 20% lub mniej do końca 2026 roku (liczone na podstawie zmian w plikach oraz dodanych/usuniętych linii)
  • Zdrowie architektury

    • Metryki dotyczące zgodności ze wzorcami, sprzężenia technologicznego, naruszeń granic
  • Efektywność review

    • Mniejsze PR-y, szybsze review, mniej rund feedbacku
  • Dostępność aplikacji i API

    • Jak blisko jesteśmy 99.99%?
Keith Williams

Post by

Keith Williams

Head of Engineering