Code review dla projektów z podwykonawcami: dlaczego „kod działa” to nie wystarczające kryterium

Przez ostatnie lata przewinęło się przez moje ręce kilkanaście projektów, gdzie główny problem nie był techniczny w sensie „system nie działa”. Problem był głębszy – kod działał na tyle dobrze, żeby przejść podstawową akceptację, ale na tyle źle, żeby stworzył dług techniczny trudny do spłacenia. Najczęściej przychodziło to z jednego źródła: podwykonawcy dostarczający kod bez rzeczywistego nadzoru architektonicznego.

Nie chodzi tu o złą wolę czy niekompetencję. Chodzi o fundamentalną różnicę między tym, co contractor rozumie jako „gotowe” a tym, co system będzie musiał przeżyć przez kolejne miesiące i lata w produkcji. Gdy podwykonawca nie jest częścią długoterminowej struktury zespołu, jego naturalną incentive jest zamknąć task i pójść dalej. Twój problem zaczyna się dokładnie w momencie, gdy jego się kończy.

Kiedy "działa" przestaje wystarczać

Większość kryzysu, który widziałem w projektach z zewnętrznymi developerami, zaczyna się od tego samego błędnego założenia: jeśli feature przechodzi przez podstawowe testy manualne i user story jest „completed”, to można mergować. Ten moment akceptacji zawsze wydaje się rozsądny – przecież mamy deadline, klient czeka, a kod faktycznie robi to, co miał robić.

Problem ujawnia się znacznie później. Czasem tydzień po deployu, czasem miesiąc, czasem dopiero gdy próbujesz dodać następny feature i okazuje się, że musisz refaktorować pół systemu, bo poprzednia implementacja była tak mocno coupled, że nie da się jej rozszerzyć bez przepisania od nowa.

Klasyczny case: projekt WordPress z customowym checkout flow. Podwykonawca dostarczył działającą bramkę płatności. Faktycznie działała – płatności przechodziły, webhooks przychodziły, ordery się zamykały. Kod przeszedł testy manualne. Problem był w tym, że cała logika integracji z payment gateway była wpleciona bezpośrednio w template checkout page. Hardcoded API credentials, brak abstrakcji, zero możliwości dodania drugiej bramki bez kopiowania całego kodu. Odkryliśmy to dopiero gdy klient – dwa tygodnie po launchu – poprosił o dodanie PayPal jako alternatywy. Estimate: 3 dni. Rzeczywistość: przepisywanie całego payment flow, bo nie dało się go rozszerzyć.

To nie był sabotaż. To był efekt braku wyraźnych guardrails architektonicznych i traktowania „feature works” jako jedynego kryterium akceptacji.

Technical debt ukryty w działającym kodzie

Największe niebezpieczeństwo z kodem od contractorów nie jest w tym, co się psuje od razu. Jest w tym, co psuje się stopniowo albo ujawnia dopiero przy skalowaniu lub zmianie wymagań.

Typowe symptomy widziałem w kilku kategoriach. Pierwszy: logika biznesowa wpleciona w warstwę prezentacji. Nie mówię tu o MVC purism – mówię o sytuacjach, gdzie query do bazy, transformacja danych i rendering HTML siedzą w jednej 200-liniowej funkcji bez żadnego separation of concerns. Kod działa, ale każda zmiana wymaga edycji trzech rzeczy na raz, testy są prawie niemożliwe do napisania, a debugging przypomina rozplątywanie spaghetti.

Drugi symptom: brak jakiejkolwiek error handling strategy. Happy path działa świetnie. Problem w tym, że produkcja nie składa się z happy pathów. External API timeout? Uncaught exception i biały ekran. Invalid input data? Fatal error zamiast graceful degradation. Database connection spike? Całkowity crash zamiast retry logic. Ten typ problemów wychodzi dopiero pod realnym traffikiem, bo na stagingu wszystko jest idealne – baza jest blisko, API odpowiada w 50ms, dane są zawsze poprawne.

Trzeci: dependency hell. Contractor często instaluje to, co zna albo co szybko rozwiązuje problem. Rezultat: projekt ma 3 różne biblioteki do HTTP requests, 2 competing state management solutions, i mieszankę deprecated packages, które działają na PHP 7.4 ale blokują upgrade do 8.x. Problem nie jest widoczny w momencie delivery – dopiero gdy próbujesz zaktualizować framework albo załatać security vulnerability i okazuje się, że pół stacku się sypie.

Czwarty symptom, szczególnie widoczny przy kodzie generowanym przez AI: brak spójności wzorców. Część aplikacji używa hookow WordPress, część modyfikuje core functions bezpośrednio. Część używa REST API, część zwraca JSON z custom endpoints bez żadnej walidacji. Część używa prepared statements, część konkatenuje SQL stringi. Każda sekcja kodu działa w izolacji, ale razem tworzą architektoniczny chaos, gdzie nie ma żadnej przewidywalności. Debugowanie takiego systemu przypomina grę w rosyjską ruletkę – nigdy nie wiesz, którą konwencję akurat zastosowano w danym miejscu.

W pewnym momencie pracowałem z klientem, który miał MVP platformy e-commerce zbudowane przez agencję zewnętrzną. System działał, sprzedaż szła, klient był zadowolony. Przyszli do nas, bo chcieli dodać program lojalnościowy – teoretycznie prosty feature. Gdy zaczęliśmy analizować kod, okazało się, że user accounts, cart logic i order processing były tak mocno zahardcodowane i coupled, że niemożliwe było dodanie punktów lojalnościowych bez refaktorowania połowy checkout flow. Klient był zaskoczony – przecież „wszystko działało”. Działało, ale architektura była tak sztywna, że każda zmiana wymagała przepisywania zamiast rozszerzania. To jest właśnie moment, gdy „kod działa” przestaje wystarczać jako kryterium.

Automated quality gates jako pierwsza linia obrony

Pierwsze co wdrażam w projektach z zewnętrznymi developerami to CI pipeline z quality gates, które blokują merge jeśli kod nie spełnia minimalnych standardów. Nie jest to kwestia zaufania – jest to kwestia wymuszenia spójności na poziomie, gdzie human review jest za wolny i za subiektywny.

Podstawa to static analysis i linting enforcement. Dla WordPress projektu to PHP_CodeSniffer z WordPress Coding Standards, dla JavaScript to ESLint z odpowiednim configiem. Kluczowe jest, żeby te narzędzia były skonfigurowane ściśle i żeby fail blokował merge, nie tylko generował warnings. Widziałem zbyt wiele projektów, gdzie linter był „zalecany” ale nie wymuszony – rezultat zawsze był taki sam: chaos stylistyczny i połowa zespołu ignorująca rules.

Przykład configuracji, która faktycznie łapie problemy:

# .github/workflows/quality-check.yml

- name: PHP CodeSniffer

run: |

phpcs --standard=WordPress --extensions=php \

--ignore=*/vendor/*,*/node_modules/* \

--report=summary \

--error-severity=1 \

--warning-severity=8 \

.

# Fail on ANY error or high-severity warning

To nie jest o code style purism. To o wymuszeniu minimalnej spójności, która sprawia, że kod jest maintainable przez kogoś innego niż oryginalny autor. Gdy contractor dostarcza kod, który przechodzi przez takie gates, już masz pewność, że przynajmniej powierzchniowo trzyma się konwencji projektu.

Drugi layer to security scanning. Dependabot albo Snyk zintegrowane z CI, które blokują merge jeśli PR wprowadza zależności ze znanymi vulnerabilities. Contractors rzadko myślą o security implications swoich wyborów – instalują to, co działa, bez sprawdzania CVE database. Automated scanning to catch tego typu problemów zanim trafią do produkcji. Szczególnie ważne w ekosystemie PHP i JavaScript, gdzie dependency hell jest normą, a updates security patches to ciągły wyścig.

Trzeci element to test coverage thresholds. Wymuszenie minimum 60-70% unit test coverage dla nowego kodu. Nie chodzi o coverage metric jako cel sam w sobie – chodzi o to, żeby contractor był zmuszony napisać kod testowwalny. Jeśli nie da się go przetestować, prawdopodobnie jest źle zaprojektowany – zbyt coupled, zbyt dużo side effects, zbyt mało separation of concerns. Wymuszenie testów naturalnie prowadzi do lepszej architektury, bo testowwalny kod zwykle jest też maintainable code.

Czwarte: performance budgets. Automated lighthouse checks albo load testing, które blokują merge jeśli nowy kod degraduje performance poniżej ustalonego threshold. Contractors często nie myślą o performance implications – dla nich „działa” znaczy „feature responds”. Nie weryfikują, czy ten response time to 100ms czy 3 sekundy. Automated performance testing łapie tego typu regresje zanim trafią do produkcji.

Przykład prostego performance gate:

// lighthouserc.js

module.exports = {

ci: {

assert: {

assertions: {

"categories:performance": ["error", {minScore: 0.8}],

"first-contentful-paint": ["error", {maxNumericValue: 2000}],

"interactive": ["error", {maxNumericValue: 3500}]

}

}

}

};

Kluczowa różnica między projektem z quality gates a projektem bez: w pierwszym przypadku większość oczywistych problemów nie dociera nawet do human review, bo są automatycznie blokowane. W drugim przypadku reviewer musi łapać wszystko ręcznie, co jest wolne, męczące i error-prone. Automated gates to nie replacement dla code review – to filtr, który sprawia, że human review może się skupić na tym, co naprawdę wymaga ludzkiego judgement: architektura, business logic, edge cases.

Co łapie code review, czego automation nie złapie

Gdy kod już przejdzie przez automated gates, zaczyna się właściwa praca. Human review ma łapać problemy architektoniczne i design decisions, które nie są wykrywalne przez linter czy test suite.

Pierwsze na co patrzę: architectural patterns i separation of concerns. Czy kod respektuje warstwową strukturę aplikacji, czy miesza logikę biznesową z prezentacją? Czy jest odpowiednie separation między data access, business logic i presentation? Linter tego nie złapie, bo składniowo kod jest poprawny. Dopiero human może ocenić, czy sposób zorganizowania kodu ma sens w kontekście całego systemu.

Konkretny red flag: god objects albo god functions. Klasy, które robią za dużo rzeczy na raz. Funkcje powyżej 100 linii z multiple responsibilities. Symptom: gdy czytasz kod i musisz kilka razy scrollować żeby zrozumieć, co się dzieje, albo gdy funkcja nazywa się „processOrder()” ale robi też validation, payment processing, email sending i inventory update. Wszystko w jednym bloku. To działa, ale jest niemaintainable. Każda zmiana w jednym aspekcie wymaga modyfikacji całej funkcji, co zwiększa ryzyko regresu w innych częściach.

Drugi red flag: tight coupling i brak abstrakcji. Kod, który bezpośrednio odwołuje się do konkretnych implementacji zamiast używać interfejsów albo dependency injection. Przykład z projektu WordPress: payment processing code, który bezpośrednio wywołuje Stripe API. Nie ma żadnej warstwy abstrakcji. Co się stanie, gdy klient zechce dodać PayPal albo inną bramkę? Trzeba będzie duplikować całą logikę. Lepsze podejście to payment gateway interface i konkretne implementacje – dodanie nowej bramki to wtedy kwestia nowej klasy implementującej interfejs, nie modyfikowania istniejącego kodu.

Trzeci problem: brak error handlingu albo naiwny error handling. Kod zakłada, że external dependencies zawsze odpowiedzą, że database zawsze będzie dostępna, że input data zawsze będą valid. Produkcja weryfikuje te założenia bardzo szybko i bardzo boleśnie. Szukam miejsc, gdzie brakuje try-catch bloków wokół external calls, gdzie nie ma timeout configuration, gdzie brak graceful degradation. Jeśli API nie odpowiada, czy system crash”uje czy pokazuje użytkownikowi sensowny komunikat i loguje błąd do monitoringu?

Czwarty aspekt: security vulnerabilities, które nie są łapane przez automated scanning. SQL injection poprzez konkatenację stringów zamiast prepared statements. XSS przez nieescapowany output. CSRF brak tokens w formach. Authorization bugs, gdzie endpoint sprawdza czy user jest zalogowany, ale nie sprawdza czy ma uprawnienia do konkretnego zasobu. Authentication bypass przez błędną logikę. To nie są rzeczy, które static analyzer złapie, bo składniowo kod jest poprawny – problem jest w logice, nie w składni.

Piąte: dokumentacja i knowledge transfer. Czy kod jest self-documenting czy wymaga 30 minut studiowania żeby zrozumieć, co robi? Czy złożone business logic decisions są wyjaśnione w komentarzach? Czy nietypowe podejścia mają uzasadnienie? Contractors często dostarczają kod bez żadnej dokumentacji, zakładając że „kod się sam tłumaczy”. Problem w tym, że za pół roku, gdy oryginalny autor już nie będzie dostępny, ktoś będzie musiał to debugować albo rozszerzać. Brak dokumentacji to technical debt, który kumuluje się z czasem.

W praktyce code review to nie tylko przeczytanie PR-a. To systematyczne przejście przez checklist architektoniczny specyficzny dla stacku. Dla WordPress projektu sprawdzam czy kod używa hooków i filtrów zamiast modyfikować core, czy wykorzystuje CPT i meta fields zamiast custom tables, czy enqueuuje assets poprawnie. Dla React projektu sprawdzam component composition, state management patterns, czy nie ma prop drilling przez 5 poziomów. Dla każdego stacku są specyficzne anty-wzorce, które trzeba aktywnie wyłapywać.

Documentation jako część Definition of Done

Jeden z problemów, który widzę najczęściej: contractor dostarcza działający kod bez jakiejkolwiek dokumentacji tego, jak działa i dlaczego jest zaprojektowany w określony sposób. Za miesiąc albo dwa, gdy trzeba coś zmienić albo naprawić, nikt nie pamięta kontekstu. Jedyna opcja to reverse engineering – czytanie kodu i próba wywnioskowania intencji autora. To działa, ale jest wolne i error-prone.

Wymuszam dokumentację jako hard requirement w Definition of Done. Nie chodzi o pisanie powieści – chodzi o minimum informacji, które pozwolą następnemu developerowi zrozumieć kontekst bez spędzania godzin na detektywistyce.

Konkretnie: każdy nietypowy design decision musi mieć komentarz tłumaczący dlaczego. Jeśli implementacja wygląda nieintuicyjnie, ale jest taka z konkretnego powodu (performance, compatibility, constraint zewnętrzny), to musi być wyjaśnione. Jeśli używasz workaround dla buga w bibliotece zewnętrznej, dodaj link do issue na GitHubie. Jeśli logika biznesowa jest złożona, wyjaśnij ją w block comment nad funkcją.

To nie jest tylko o code comments. To też o Architecture Decision Records dla większych zmian. Jeśli contractor dodaje nową integrację albo zmienia sposób działania krytycznego flow, wymuszam krótki ADR tłumaczący problem, rozważane opcje, wybraną opcję i trade-offy. To brzmi jak overhead, ale w praktyce oszczędza ogromne ilości czasu później, gdy ktoś będzie musiał zrozumieć dlaczego system jest zaprojektowany w określony sposób.

Ostatni element: knowledge transfer session jako część DoD. Przed zaakceptowaniem większego feature”a wymagam 30-minutowego walkthroughu, gdzie contractor tłumaczy internal team co i jak zaimplementował. To nie jest formalne presentation – to bardziej pair debugging session, gdzie zadajemy pytania i weryfikujemy, że rozumiemy kod na tyle, żeby móc go maintainować. Jeśli contractor nie jest w stanie wytłumaczyć swojej implementacji w sposób zrozumiały dla innych, to red flag, że albo kod jest za bardzo skomplikowany, albo autor sam nie do końca rozumie co napisał.

Widziałem wiele sytuacji, gdzie ten knowledge transfer ujawnił fundamentalne misunderstandings. Contractor myślał, że implementuje feature A w określony sposób, a w rzeczywistości business requirements były inne. Albo użył biblioteki X zakładając że działa w sposób Y, ale faktycznie działa inaczej i jego kod po prostu „udawał” że działa przez szczęśliwy zbieg okoliczności. Te problemy wychodzą dopiero gdy próbujesz wytłumaczyć kod komuś innemu – sam process tłumaczenia wymusza clarification i często ujawnia błędne założenia.

"Po kilku takich projektach człowiek zaczyna podejrzewać, że większość technical debt nie powstaje podczas developmentu. Po prostu bardzo długo udaje działającą funkcjonalność."

Architecture guidelines jako north star

Największy problem z contractors to brak długoterminowego kontekstu. Oni widzą swój task, nie widzą całości systemu. Ich naturalny instynkt to rozwiązać problem najszybszym dostępnym sposobem, co często prowadzi do solutions, które działają lokalnie ale nie pasują do większej architektury.

Rozwiązanie to jasno zdefiniowany golden path – przykładowa implementacja, która pokazuje jak rzeczy powinny być robione w tym konkretnym projekcie. Nie abstrakcyjna dokumentacja architektoniczna, ale konkretny, działający kod, który można skopiować i zaadaptować. Dla WordPress projektu to przykładowy custom plugin pokazujący jak poprawnie używać hooków, filtrów, CPT i meta fields. Dla React aplikacji to example component pokazujący preferowany pattern state management, prop types, error boundaries.

Contractor dostaje zadanie plus link do golden path example i jasny komunikat: „zaimplementuj to w podobny sposób”. To redukuje cognitive load – nie musi wymyślać architektury od zera, tylko zaadaptować proven pattern. To też sprawia, że code review jest prostszy – możesz szybko sprawdzić czy nowa implementacja trzyma się ustalonego wzorca czy odbiega od niego.

Drugi element to Architecture Decision Records dokumentujące większe decyzje architektoniczne. Jeśli zdecydowaliśmy, że używamy REST API zamiast GraphQL, jest ADR tłumaczący dlaczego. Jeśli zdecydowaliśmy się na konkretny state management solution, jest ADR pokazujący rozważane opcje i trade-offy. Contractor czytając ADR rozumie nie tylko „co” ale i „dlaczego”, co redukuje prawdopodobieństwo, że zaproponuje solution idącą w całkowicie innym kierunku.

Trzeci aspekt: jasno zdefiniowane no-go zones. Lista rzeczy, których absolutnie nie wolno robić w tym projekcie. Dla WordPress projektu: nie modyfikuj core files, nie twórz custom database tables bez approval, nie instaluj pluginów bez security review. Dla każdego stacku są specyficzne praktyki, które chcesz zakazać albo przynajmniej wymagać explicit approval przed użyciem.

W pewnym momencie pracowaliśmy z clientem, który chciał dodać live chat do swojej platformy. Podwykonawca wrócił z propozycją integracji przez iframe embedding zewnętrznego widget. Technicznie działało, ale było to kompletnie against architectural guidelines projektu, gdzie explicitly unikaliśmy third-party scripts i iframes ze względów bezpieczeństwa i performance. Gdybyśmy nie mieli jasno udokumentowanych guidelines, contractor prawdopodobnie zaimplementowałby to i dostarczył jako „gotowe”. Dzięki ADR tłumaczącemu dlaczego unikamy third-party embeds, mogliśmy od razu powiedzieć „nie, znajdź inne podejście” i zaoszczędzić czas na reimplementację. Ostatecznie zintegrowaliśmy chat przez API zamiast widget, co było bardziej work ale aligned z architekturą systemu.

Remediation: gdy zły kod już jest w produkcji

Najgorsza sytuacja to odkrycie, że contractor dostarczył kod, który przeszedł przez review (albo review nie było wystarczająco ścisłe) i już jest w produkcji, a teraz zaczynają się problemy. System działa, ale widać performance degradation albo pojawiają się bugs, które są trudne do zdiagnozowania, albo próbujesz dodać nowy feature i okazuje się że obecna architektura na to nie pozwala.

Pierwszy impuls to zwykle „przepiszmy to od nowa”. Problem w tym, że complete rewrite jest zwykle najgorszą możliwą opcją – długi, ryzykowny, blokujący inne features. Lepsze podejście to incremental refactoring, ale wymaga to priorytetyzacji – nie możesz refaktorować wszystkiego naraz.

Używam prostej risk/impact matrix do priorytetyzacji technical debt. Oś X to „jak bardzo ten kod blokuje development nowych features” (impact), oś Y to „jakie jest ryzyko, że ten kod spowoduje production incident” (risk). Kod w prawym górnym kwadrancie (high risk, high impact) refaktorujesz natychmiast. Kod w lewym dolnym (low risk, low impact) zostawiasz w spokoju. Kod w pozostałych kwadrantach obsługujesz oportunistycznie – gdy już i tak edytujesz ten area systemu, robisz przy okazji refactoring.

Drugi aspekt to strangler fig pattern – stopniowe zastępowanie złych komponentów nowymi, bez riskowania big bang migration. Przykład: masz źle zaprojektowany payment processing code. Zamiast rewritować wszystko naraz, tworzysz nową abstrakcję (payment gateway interface) i stopniowo migrowasz existing code żeby z niej korzystać. Nowa funkcjonalność (np. dodanie PayPal) implementujesz już w nowej architekturze. Stara funkcjonalność (Stripe) zostaje wrapped w nowy interface ale wewnętrznie nadal używa legacy code. Stopniowo refactorujesz legacy implementation, ale w każdym momencie system działa i jest deployable.

Trzeci element to circuit breaker pattern dla ryzykownego legacy code. Jeśli masz obszar systemu, który jest źle napisany i potencjalnie unstable, ale nie możesz go natychmiast przepisać, owijasz go w circuit breaker. Przy pierwszych oznakach problemów (timeouts, error rate spike) circuit breaker otwiera się i system wraca do fallback behavior zamiast cascading failure. To nie rozwiązuje problemu, ale ogranicza jego blast radius podczas gdy pracujesz nad proper fix.

Czwarte: monitoring i observability jako priorytet przy problematycznym legacy code. Jeśli wiesz, że dany obszar systemu jest technical debt, ale musi poczekać na refactoring, przynajmniej dodaj tam szczegółowe logi i metryki. Gdy coś się psuje, chcesz móc szybko zdiagnozować problem zamiast debugować w ciemno. Structured logging z odpowiednim context, distributed tracing jeśli to distributed system, alerts na nietypowe patterns. To nie rozwiązuje debt, ale znacząco redukuje MTTR gdy problemy się pojawią.

Ostatni element to learning loop – analiza dlaczego dany kod przeszedł przez review mimo problemów. Czy był to brak automated gates? Niedostatecznie szczegółowy code review? Źle zdefiniowane acceptance criteria? Brak architectural guidelines? Każdy incident to okazja żeby wzmocnić guardrails tak, żeby podobny problem nie powtórzył się w przyszłości. To nie jest o winieniu ludzi – to o poprawianiu procesu.

Layer 3: architectural review dla kritycznych zmian

Automated gates łapią syntax i oczywiste anti-patterns. Code review łapie design issues i implementation problems. Ale są zmiany, które potrzebują jeszcze wyżej poziomu review – architectural review, gdzie patrzysz nie na konkretny kod, ale na sposób w jaki nowa zmiana wpływa na cały system.

Nie każdy PR wymaga architectural review. Wymuszam go dla kilku kategorii zmian: nowe integracje z external systems, zmiany w core business logic flow, dodawanie nowych dependencies albo frameworks, znaczące zmiany w database schema, modyfikacje w authentication/authorization logic.

Architectural review to nie jest przeczytanie kodu – to analiza konsekwencji. Jak ta zmiana wpływa na system boundaries? Czy wprowadza nowe failure modes? Czy zwiększa coupling między komponentami? Czy tworzy bottlenecks albo single points of failure? Czy jest zgodna z długoterminową wizją architektury systemu?

Konkretny przykład: contractor proponuje dodanie Redis jako cache layer. Kod wygląda dobrze, implementacja jest poprawna, automated tests przechodzą. Ale architectural review ujawnia problem: wprowadzenie Redis jako critical dependency zmienia failure characteristics całego systemu. Co się stanie, gdy Redis przestanie odpowiadać? Czy system gracefully degraduje do działania bez cache, czy całkowicie się crashuje? Jak to wpływa na deployment process – czy teraz wymagamy Redis na wszystkich environments? Jak to zmienia operational complexity? Te pytania nie są widoczne na poziomie konkretnego PR-a – wymagają myślenia o całym systemie.

Drugi aspekt architectural review to consistency checking. Czy nowe podejście jest consistent z tym, jak rozwiązywaliśmy podobne problemy wcześniej? Jeśli już mamy established pattern dla konkretnego typu problemów, nowa implementacja powinna go używać chyba że jest explicit reason żeby odejść od wzorca. Inconsistency w architekturze to technical debt – za rok nikt nie będzie pamiętał dlaczego feature X używa solution A a feature Y używa solution B, i debugowanie będzie confused przez brak unified approach.

Trzeci element to capacity planning i scalability analysis. Czy nowa implementacja skaluje się razem z resztą systemu? Jeśli obecnie masz 100 users i 1000 transactions dziennie, ale planujesz 10x growth, czy ta architektura będzie działać przy 1000 users i 10k transactions? Contractor często nie ma tego kontekstu – implementuje pod current requirements, nie pod future growth. Architectural review ma łapać takie assumptions i albo zaakceptować je explicitly (jeśli wiesz, że będziesz refaktorować przy skalowaniu) albo wymusić bardziej skalowalne podejście teraz.

"To był moment, w którym po raz kolejny okazało się, że „działa” i „da się utrzymać” to dwie zupełnie różne właściwości systemu. Pierwszą da się sprawdzić na demo. Drugą zwykle testuje dopiero produkcja, kilka miesięcy później."

Co dalej z wiedzą gdy contractor odchodzi

Problem który często się pojawia: projekt jest delivered, contractor znika, a po miesiącu czy dwóch zaczynają się problemy albo potrzeby zmian. Nikt nie pamięta dlaczego kod jest napisany w określony sposób, dokumentacja jest minimalna, original author nie jest dostępny. Maintenance staje się archeologią – próbujesz wywnioskować intencje autora z kodu i commit messages.

Jedyna realna defense to code ownership i knowledge transfer zorganizowany systematycznie, nie ad-hoc. W praktyce to oznacza, że każdy obszar systemu musi mieć designated owner z internal teamu, który jest review”erem każdego PR w tym obszarze i który uczestniczy w knowledge transfer sessions. To nie musi być głęboka ekspertyza – wystarczy, że rozumie architekturę i flow na tyle, żeby móc triage”ować problemy i guidance dla następnych developerów.

Drugi aspekt to runbooki i playbooki dla operational tasks. Jak deployment działa krok po kroku? Jak rollback w przypadku problemu? Jakie są common failure modes i jak je debugować? Co sprawdzać najpierw gdy system się psuje? Te informacje powinny być udokumentowane zanim contractor odejdzie, nie potem. W praktyce wymuszam tworzenie runbooka jako część DoD dla każdego deployed feature”a – jeśli nie umiesz wyjaśnić jak to operować, feature nie jest gotowy.

Trzeci element to postmortem process po każdym production incident. Jeśli coś poszło nie tak, robisz postmortem który dokumentuje co się stało, dlaczego się stało, i co robisz żeby się nie powtórzyło. To tworzy historical record problemów i ich solutions, który jest bezcenny dla future maintenance. Contractor często nie będzie dostępny do postmortemu (bo już go nie ma), ale wtedy postmortem robi internal team i dokumentuje co odkrył debugując problem. To accumulates knowledge zamiast tracić ją gdy ludzie odchodzą.

Governance bez blokowania velocity

Największe wyzwanie to balance między kontrolą jakości a delivery speed. Zbyt luźne guardrails i dostajesz technical debt. Zbyt ścisłe guardrails i każdy PR siedzi w review przez tydzień, velocity spada, contractors frustują się, deadlines się rozjeżdżają.

Strategia którą widziałem działać to „layered governance”. Podstawowe rzeczy – linting, security scanning, test coverage – są automated i non-negotiable. Jeśli nie przechodzisz, nie mergujesz. Tu nie ma dyskusji ani exceptions. Automated gates są szybkie, deterministyczne, jasne. Nikt nie czuje się micromanaged przez lintera.

Warstwa wyższa – code review – ma flexible timeline dependency od rozmiaru PR-a. Small PRs (poniżej 200 linii) mają SLA 24h. Medium PRs (200-500 linii) mają SLA 48h. Large PRs (powyżej 500 linii) albo są rozbijane na mniejsze, albo mają explicitly extended review time. To tworzy incentive dla contractors żeby nie dostarczać giant PRs tuż przed deadline – wiedzą, że będzie czekał w kolejce.

Jeszcze wyższa warstwa – architectural review – jest reserved tylko dla kritycznych zmian. Większość PRs jej nie wymaga. To prevents bottleneck gdzie każdy drobny change musi czekać na senior architect approval. Trust but verify – większość kodu przechodzi przez standardowy review, tylko critical paths wymagają głębszej analysis.

Kluczowe jest też dawanie feedback early i często. Jeśli widzisz że contractor idzie w złym kierunku, nie czekasz aż dostarczy feature – dajesz feedback w momencie gdy widzisz problem w draft PR albo design proposal. Refaktorowanie draft-a jest tańsze niż refaktorowanie completed feature. To wymaga proactive monitoring work in progress, nie tylko reactive review completed work.

Ostatni element to retrospectives z contractors jako część normalnego procesu. Co miesiąc albo co sprint – zależnie od cadence projektu – robisz retrospective gdzie dyskutujesz co działało, co nie działało, gdzie były bottlenecki, jak usprawnić process. Contractors często mają valuable feedback o tym, co utrudnia im pracę albo co jest unclear w guidelines. To nie jest micromanagement – to continuous improvement process, gdzie obie strony uczą się jak współpracować efektywnie.

Lekcje

Ostatecznie, cały ten process nie jest o braku zaufania do contractors. Jest o fundamentalnej różnicy incentives między kimś, kto dostarcza feature i idzie dalej, a zespołem który będzie żył z tym kodem przez kolejne lata. Contractor nie robi nic złego gdy optymalizuje pod delivery speed – to jest jego job. Twój job to upewnić się, że ta delivery speed nie przekłada się na technical debt, który będzie kosztował cię znacznie więcej później.

Production ma sposób na bardzo szybką weryfikację założeń o jakości kodu. Możesz myśleć że system jest solid, bo przechodzi testy i działa na stagingu. Prawda wychodzi dopiero pod realnym traffikiem, z prawdziwymi userami robiącymi nieoczekiwane rzeczy, z external services które czasem nie odpowiadają, z edge cases których nikt nie przewidział. Im więcej guardrails wbudowałeś wcześniej, tym mniej niespodzianek dostajesz później. To nie jest kwestia perfekt – to kwestia minimum viable quality, które pozwala systemowi przeżyć kontakt z rzeczywistością bez kompletnej implozji.

Podsumowanie

Kod od podwykonawcy zwykle działa. Przynajmniej do momentu, w którym ktoś próbuje dodać kolejny feature, zaktualizować zależność albo uruchomić system pod ruchem większym niż ten z dema sprzedażowego. Wtedy okazuje się, że większość problemów nie powstała w produkcji. One były tam od początku, tylko jeszcze nie miały okazji się ujawnić.

Najbardziej kosztowny technical debt rzadko wygląda jak awaria. Znacznie częściej wygląda jak działający system, którego nie da się już sensownie rozwijać. Każda zmiana wymaga obejścia poprzedniego obejścia, kolejne integracje zaczynają przypominać archeologię, a estymacje przestają mieć związek z rzeczywistością. Kod dalej przechodzi testy. Zespół już niekoniecznie.

Po kilku miesiącach utrzymywania takich projektów człowiek przestaje pytać, czy feature działa. Znacznie ciekawsze staje się pytanie, ile nowych problemów właśnie ukrył przed następną osobą, która będzie musiała go dotknąć.

Production ma zresztą wyjątkowy talent do wykrywania rzeczy, które na code review wyglądały całkiem rozsądnie. Szczególnie tych napisanych przez kogoś, kto już od dawna pracuje przy następnym projekcie.

Zobacz powiązane case studies i analizy

Procesy, architektura i workflow powiązane z tematami poruszanymi w tym materiale – od integracji i realtime systems po automatyzacje operacyjne.

Inne

Pozostałe artykuły

Webhook Reliability Patterns: czego nauczyłem się debugując phantom calls w systemie VoIP
Distributed state w systemach realtime działa poprawnie tylko do momentu, w którym różne warstwy infrastruktury zaczynają posiadać sprzeczne informacje o tym samym połączeniu.
Confidence score OCR często wprowadza w błąd. Sprawdź, jak skutecznie walidować dane i ograniczyć błędy w produkcji.