Martwy kod, znany też jako dead code, to fragmenty programu, które nie wykonują się w żadnej realnej ścieżce działania, ale nadal zajmują miejsce w repozytorium i w głowach zespołu. W architekturze oprogramowania nie jest to drobiazg: taki balast utrudnia refaktoryzację, zaciemnia odpowiedzialności modułów i potrafi ukrywać problemy z wdrażaniem zmian. Poniżej rozkładam temat na czynniki pierwsze: czym jest ten problem, skąd się bierze, jak go wykrywać i kiedy nie usuwać go zbyt pochopnie.
Martwy kod nie boli w chwili powstania, tylko później
- To kod, którego system nie uruchamia albo nie powinien już uruchamiać.
- Najczęściej pojawia się po refaktorach, migracjach, eksperymentach i zmianach API.
- Największy koszt to rosnąca złożoność, a nie sam rozmiar repozytorium.
- Nie każdy nieużywany fragment należy usuwać od razu, bo część ścieżek bywa celowo przejściowa.
- Najbezpieczniej usuwać po potwierdzeniu użycia, małymi krokami i z testem regresji.
Czym właściwie jest martwy kod
W praktyce rozróżniam dwa typy takich fragmentów. Pierwszy to kod nieosiągalny, czyli taki, do którego nie prowadzi już żadna ścieżka wykonania: blok po `return`, gałąź `if`, która nigdy nie zajdzie, albo stary wariant w `switch`, który przestał mieć sens po zmianie logiki. Drugi to kod nieużywany, który formalnie istnieje, ale nikt go nie wywołuje, bo nie ma już do niego odwołań.
Kod nieosiągalny
To najprostszy do zrozumienia przypadek. Jeśli funkcja kończy się wcześniej, a poniżej nadal stoi kilka instrukcji, te instrukcje nie mają już znaczenia. Podobnie jest z warunkami opartymi na starych flagach, które nie mają już żadnego źródła danych albo od dawna zawsze zwracają ten sam wynik.
Przeczytaj również: Wzorzec szablonowa metoda – kiedy upraszcza kod, a kiedy szkodzi?
Kod nieużywany
Tu problem bywa bardziej podstępny, bo fragment wygląda zdrowo. Ma nazwę, typy, importy i komentarze, ale nikt nie zna już drogi, która do niego prowadzi. W frontendzie może to być stary helper po przebudowie komponentu, a w backendzie - metoda po zmianie kontraktu API. Z perspektywy architektury nadal jest to koszt, bo każda osoba czytająca system musi go zinterpretować, zanim stwierdzi, że można go wyrzucić.
Granica jest dla mnie prosta: jeśli nie potrafię wskazać aktualnego powodu biznesowego albo technicznego, by dany fragment dalej żył, traktuję go jak kandydat do usunięcia. To prowadzi do pytania, skąd taki kod w ogóle się bierze.

Skąd bierze się nieużywany kod w systemach
Najczęściej nie pojawia się on jako jeden spektakularny błąd. Raczej jako suma małych decyzji: „zostawmy to na chwilę”, „wrócimy do tego po wdrożeniu”, „na razie nie ruszajmy, bo rollback”. W zespołach webowych widzę to szczególnie często przy refaktorach, migracjach i stopniowym włączaniu nowych funkcji.
- Refaktoryzacja bez domknięcia - stara gałąź zostaje obok nowej, bo ktoś nie chciał ryzykować usunięcia za wcześnie.
- Feature flags - świetne do kontrolowanego rollout’u, ale łatwo zamienić je w wieczne przełączniki bez właściciela.
- Branch by abstraction - dobry wzorzec do dużych migracji, ale jeśli nie ma planu końcowego, zostawia podwójną ścieżkę utrzymania.
- Zmiany kontraktów API - stare pola, stare endpointy i stare mapowania zostają „na wszelki wypadek”.
- Copy-paste i szybkie poprawki - najkrótsza droga do tego, żeby obok nowej logiki został stary wariant, którego nikt już nie sprawdza.
W architekturze problem polega na tym, że takie pozostałości nie są neutralne. Każdy nowy wyjątek, każdy warunkowy skrót i każdy ukryty fallback zwiększa liczbę rzeczy, które trzeba pamiętać przy następnej zmianie. To właśnie dlatego martwy kod ma wpływ nie tylko na repozytorium, ale na cały sposób pracy nad systemem.
Jak wpływa na architekturę i zespół
Najgorsze w takim kodzie jest to, że przez długi czas nic się nie psuje. A mimo to system robi się cięższy w utrzymaniu. Jak przypomina MDN, tree shaking usuwa nieużywane fragmenty podczas bundlowania modułów, ale to rozwiązuje głównie objaw po stronie paczki. W źródłach nadal trzeba rozumieć, co jest żywe, a co tylko zostało po wcześniejszej wersji projektu.
| Obszar | Co się dzieje | Dlaczego to boli |
|---|---|---|
| Czytelność | W plikach zostaje więcej gałęzi, helperów i wyjątków. | Reviewer i nowa osoba w zespole tracą czas na odróżnianie logiki od historycznych resztek. |
| Testy | Utrzymujesz scenariusze, które nie powinny już istnieć. | Rośnie liczba testów bez realnej wartości, a pokrycie zaczyna dawać złudne poczucie bezpieczeństwa. |
| Bezpieczeństwo | Zostają stare zależności, debugowe ścieżki albo nieużywane role. | Trudniej aktualizować biblioteki i łatwiej przeoczyć fragment, który nadal otwiera niepotrzebną powierzchnię ataku. |
| Wydajność frontu | Bundle bywa większy, a importy dłużej się ładują. | Na słabszych urządzeniach i wolniejszych łączach każdy zbędny fragment pogarsza odczuwalny start aplikacji. |
| Zmiana architektury | Diagramy i dokumentacja przestają odpowiadać rzeczywistości. | Zespół planuje kolejne kroki na błędnym obrazie systemu, więc decyzje stają się coraz mniej precyzyjne. |
Właśnie dlatego patrzę na martwy kod jak na sygnał architektoniczny, a nie tylko na estetyczny problem. Jeśli w systemie rośnie liczba nieużywanych ścieżek, to zwykle znaczy, że proces zmian nie domyka się tak dobrze, jak powinien. Żeby nie zgadywać, potrzebny jest prosty sposób wykrywania.
Jak wykrywam go w praktyce
- Szukam referencji w całym repozytorium. Jeśli metoda, komponent albo endpoint występuje tylko w definicji, to pierwszy sygnał ostrzegawczy.
- Patrzę na telemetrykę i logi. W ruchu produkcyjnym szybko widać, czy dana ścieżka faktycznie dostaje ruch, czy tylko wygląda na żywą.
- Analizuję ostrzeżenia statyczne. Nieużywane importy, zmienne i gałęzie warunkowe są tanie do wychwycenia, jeśli potraktujesz warningi serio, a nie jako dekorację.
- Sprawdzam bundle i wynik kompilacji. W frontendzie to często najszybszy sposób na wyłapanie pozostałości po starych importach i modułach.
- Porównuję kod z kontraktem systemu. Jeśli API, routing albo konfiguracja już się zmieniły, a fragment nadal odwołuje się do starego świata, to zwykle mamy do czynienia z resztką po migracji.
Najwięcej czasu oszczędza mi połączenie prostego wyszukiwania z danymi z runtime. Sama analiza statyczna bywa zbyt zachowawcza, a same logi - zbyt ślepe na fragmenty, których jeszcze nikt nie uruchomił. I tu pojawia się ważne zastrzeżenie: nie wszystko, co wygląda na martwe, rzeczywiście takie jest.
Kiedy nie usuwać go od razu
Nie wycinam ślepo każdego fragmentu, którego nie widzę w logach. Czasem kod jest celowo „uśpiony”, bo wspiera migrację, rollback albo stopniowe wdrożenie. Martin Fowler opisuje release toggles jako rozwiązanie przejściowe, zwykle żyjące nie dłużej niż tydzień czy dwa, i to jest dobra praktyczna granica. Jeśli przełącznik nie ma właściciela ani daty wygaśnięcia, przestaje pomagać, a zaczyna produkować dług techniczny.
| Sytuacja | Co robię | Kiedy usuwam |
|---|---|---|
| Flaga funkcji podczas rollout’u | Zostawiam ją, ale nadaję właściciela i termin wyłączenia. | Po pełnym wdrożeniu, gdy metryki są stabilne i nie ma już potrzeby cofania zmian. |
| Branch by abstraction przy migracji | Trzymam obie implementacje tylko tak długo, jak naprawdę działa przełączenie między nimi. | Gdy wszystkie wywołania przejdą na nową ścieżkę i stara nie ma już odbiorców. |
| Rollback po ryzykownym wdrożeniu | Zostawiam stary kod do momentu ustabilizowania releasu. | W najbliższym oknie porządkowym, a nie „kiedyś później”. |
| Eksperyment albo test A/B | Pozostawiam ścieżkę tylko na czas pomiaru. | Po zakończeniu eksperymentu, gdy wynik został zebrany i decyzja podjęta. |
Jeśli nie potrafię odpowiedzieć na pytanie „po co to jeszcze żyje?”, zwykle oznacza to, że mechanizm przejściowy już dawno zamienił się w zwykłą zwłokę organizacyjną. Gdy wiem już, co warto zostawić chwilowo, mogę przejść do bezpiecznego sprzątania reszty.
Jak usuwać go bez ryzyka
- Potwierdzam brak realnego wejścia. Sprawdzam referencje, logi, metryki i routing, zanim cokolwiek skasuję.
- Wycinam najmniejszy możliwy fragment. Lepiej usunąć jedną gałąź, jedną metodę albo jeden import niż robić duży porządkowy rewrite.
- Dodaję test regresji. Jeśli kod naprawdę był potrzebny, test ujawni to od razu po zmianie.
- Porządkuję zależności po kolei. Najpierw odcinam użycie, potem usuwam implementację, a na końcu sprzątam konfigurację, dokumentację i ewentualne flagi.
- Sprawdzam wpływ na bundle i build. W frontendzie i build pipeline różnica po usunięciu resztek bywa widoczna szybciej, niż się wydaje.
- Domykam migracje w modelu expand-contract. Najpierw dodaję nową ścieżkę, potem ją włączam, a dopiero na końcu wycinam starą, gdy mam pewność, że rollback nie będzie potrzebny.
To podejście działa, bo nie opiera się na heroicznych czystkach, tylko na małych, odwracalnych krokach. W praktyce właśnie taka dyscyplina odróżnia system, który dojrzewa, od systemu, w którym resztki po kolejnych zmianach zaczynają przejmować kontrolę nad projektem. Najlepsza profilaktyka zaczyna się jednak jeszcze wcześniej.
Najlepsza obrona przed martwym kodem zaczyna się przed implementacją
Jeśli mam zostawić jedną praktyczną zasadę, to taką: każdy fragment tymczasowy powinien mieć termin ważności. Flagi, obejścia migracyjne i alternatywne ścieżki są potrzebne, ale tylko wtedy, gdy ktoś za nie odpowiada i wie, kiedy je usunąć. Bez tego „tymczasowe” bardzo łatwo staje się stałe.
- Nadaję przejściowym mechanizmom właściciela i datę wygaśnięcia.
- W migracjach planuję moment usunięcia starej ścieżki tak samo dokładnie jak moment uruchomienia nowej.
- W review pytam wprost: kto i kiedy będzie wywoływał ten kod za trzy miesiące.
- Po większych refaktorach sprawdzam, czy w repo nie zostały importy, komponenty i helpery bez odbiorców.
- W frontendzie regularnie patrzę na rozmiar bundla, bo tam zbędne fragmenty szybko wychodzą na jaw.
Jeżeli pilnujesz tych kilku zasad, martwy kod przestaje wracać w tych samych miejscach, a architektura robi się prostsza nie przez kosmetykę, tylko przez lepsze decyzje. I właśnie o to chodzi: nie o bezwzględne wycinanie wszystkiego, lecz o świadome utrzymywanie tylko tych ścieżek, które nadal mają uzasadnienie w systemie.