Monorepo to jedno repozytorium dla wielu aplikacji i bibliotek, na przykład strony, aplikacji mobilnej i API. Wyjaśniamy, co zyskuje na nim zespół, ile to kosztuje w praktyce, jakie narzędzia pomagają nad nim zapanować i w jakich sytuacjach lepiej zostać przy osobnych repozytoriach.
Monorepo to sposób organizacji kodu, w którym wiele aplikacji i bibliotek żyje w jednym repozytorium, czyli jednym, wspólnie wersjonowanym miejscu przechowywania kodu. Strona internetowa, aplikacja mobilna, API i panel administracyjny mogą leżeć obok siebie, dzielić te same komponenty i zmieniać się w jednym kroku. Brzmi jak kwestia porządku na dysku. W praktyce ta decyzja wpływa na to, jak szybko zespół wprowadza zmiany, ile kosztuje utrzymanie projektu i jak łatwo przyjąć do niego nowych ludzi. Właściciel firmy rzadko pyta, jak podzielone są repozytoria. Za to widzi skutki. Zmiana w formularzu wymaga trzech osobnych wdrożeń. Aplikacja mobilna pokazuje inne ceny niż strona, bo ktoś zapomniał zaktualizować kopię logiki. Nowy programista przez tydzień ustala, które repozytorium z czym się łączy. Część takich problemów wynika właśnie ze sposobu, w jaki kod jest rozłożony.
Czym jest monorepo i czym nie jest
Weźmy typowy produkt cyfrowy: aplikację webową w Next.js, aplikację mobilną, backend z API i panel administracyjny. W układzie z wieloma repozytoriami każda z tych części ma własne repozytorium, własną konfigurację i własną historię zmian. W monorepo wszystkie leżą w jednym, w osobnych katalogach, a wspólne elementy, takie jak typy danych, walidacja formularzy czy komponenty interfejsu, trafiają do współdzielonych pakietów. Najczęstsze nieporozumienie dotyczy słowa „mono". Monorepo nie oznacza monolitu, czyli aplikacji zbudowanej i wdrażanej jako jedna całość. W jednym repozytorium może leżeć kilkanaście niezależnie wdrażanych usług, a każda z nich może mieć własny cykl wydań. Różnice między architekturami opisuje porównanie mikroserwisów i monolitu. Sposób przechowywania kodu i sposób jego uruchamiania to dwie osobne decyzje.
Monorepo często idzie w parze z podejściem, jakim jest modularny monolit, ale równie dobrze sprawdza się przy mikroserwisach. Z jednego repozytorium korzystają zarówno małe zespoły z dwiema aplikacjami, jak i największe firmy technologiczne. Google od lat trzyma większość swojego kodu w jednym, ogromnym repozytorium z własnymi narzędziami.
Monorepo, multi-repo i warianty pośrednie
Przeciwieństwem monorepo jest multi-repo, nazywane też polyrepo: każda aplikacja i każda biblioteka ma osobne repozytorium. To podejście naturalne na starcie, bo każdy projekt zaczyna się od jednego repozytorium, a kolejne dochodzą w miarę rozwoju. Oba modele mają swoje miejsce. Szersze porównanie monorepo i multi-repo pokazuje, że decyzja zależy głównie od wielkości zespołu i od tego, jak mocno części systemu są ze sobą powiązane. W praktyce często spotykamy warianty pośrednie. Na przykład jedno repozytorium dla produktu klienta, czyli aplikacji webowej, mobilnej i API, a osobne dla niezależnych narzędzi wewnętrznych.
Niezależnie od układu podstawą jest dobrze skonfigurowany system kontroli wersji, zwykle Git. Monorepo nie zmienia narzędzia. Zmienia to, jak dużo w nim trzymamy.
Co zyskuje zespół
Największa korzyść to współdzielenie kodu bez tarcia. W modelu z wieloma repozytoriami wspólną bibliotekę trzeba opublikować jako pakiet, podbić jej wersję i zaktualizować w każdym projekcie, który z niej korzysta. W monorepo aplikacja sięga po nią bezpośrednio, a zmiana jest widoczna od razu wszędzie.
W codziennej pracy przekłada się to na kilka konkretnych rzeczy:
zmiany atomowe - jedna zmiana w kodzie może jednocześnie poprawić API, aplikację webową i mobilną, więc nie ma okresu, w którym jedna część jest już zaktualizowana, a druga jeszcze nie,
wspólne typy danych - przy projektach w TypeScript backend i frontend korzystają z tych samych definicji, a niezgodność wychodzi na etapie budowania, nie u użytkownika,
jedna biblioteka komponentów - przyciski, formularze i układy żyją w jednym pakiecie, z którego korzysta każda aplikacja,
jedna konfiguracja narzędzi - te same zasady formatowania, lintery i ustawienia testów obowiązują w całym projekcie,
łatwiejsze wejście do projektu - nowa osoba klonuje jedno repozytorium i widzi cały system.
Wspólne typy to w praktyce jeden z najmocniejszych argumentów. Zespół, który korzysta z zalet TypeScriptu w projektach, w monorepo dostaje je w całym systemie. Zmiana nazwy pola w API od razu podświetla wszystkie miejsca we frontendzie i aplikacji mobilnej, które trzeba poprawić. Podobnie jest z interfejsem. Jeśli firma ma design system, czyli spójny zestaw komponentów, monorepo pozwala utrzymywać go jako pakiet obok aplikacji, które z niego korzystają.
Monorepo nie jest darmowe. Pierwszy koszt pojawia się w procesie budowania i testowania. Jeśli każda zmiana uruchamia budowanie i testy całego repozytorium, czas oczekiwania rośnie razem z projektem. Po roku zespół czeka na wynik kilkadziesiąt minut, choć zmienił jeden tekst na stronie. Rozwiązaniem jest proces CI/CD, czyli automatyczne budowanie, testowanie i wdrażanie kodu, ustawiony tak, żeby uruchamiał tylko to, czego zmiana dotyczy. Wymaga to narzędzi, które rozumieją zależności między projektami, i wymaga czasu na konfigurację. Bez tego monorepo szybko staje się wolniejsze niż kilka osobnych repozytoriów.
Drugi koszt to uprawnienia. Git nie pozwala łatwo ograniczyć dostępu do wybranych katalogów, więc każdy, kto ma dostęp do repozytorium, zwykle widzi cały kod. Jeśli część systemu musi pozostać niedostępna dla zewnętrznego podwykonawcy, trzeba ją trzymać osobno. Da się natomiast określić, kto musi zatwierdzać zmiany w danym katalogu, na przykład plikiem wskazującym właścicieli kodu. Trzecia pułapka jest mniej widoczna. Skoro wszystko jest pod ręką, łatwo powiązać ze sobą części, które powinny pozostać niezależne. Aplikacja mobilna zaczyna importować fragment panelu administracyjnego, a po pół roku nie da się wydać jednej bez drugiej. Monorepo wymaga więc jasnych zasad, co może zależeć od czego, i narzędzi, które te zasady pilnują. Czwarta dotyczy skali. Przy bardzo dużych repozytoriach samo klonowanie i przełączanie gałęzi zaczyna trwać zauważalnie długo. Większość firm nigdy do tego etapu nie dojdzie, ale przy projektach z wieloletnią historią i dużymi plikami trzeba to uwzględnić.
Najprostszy poziom to tak zwane workspaces w menedżerach pakietów JavaScript, takich jak pnpm, npm czy Yarn. Pozwalają trzymać kilka projektów w jednym repozytorium i łączyć je ze sobą bez publikowania pakietów. Dla dwóch, trzech aplikacji to często wystarczy. Przy większej liczbie projektów przydaje się narzędzie do zarządzania zadaniami. Turborepo i Nx rozumieją zależności między pakietami, uruchamiają zadania równolegle i zapamiętują wyniki. Jeśli dany pakiet się nie zmienił, jego budowanie i testy nie są powtarzane, tylko odtwarzane z pamięci podręcznej, także współdzielonej między komputerami zespołu i serwerem CI. Nx potrafi też wskazać, które projekty zostały dotknięte zmianą, i uruchomić testy tylko dla nich.
Warto nie mylić Turborepo z Turbopackiem. Oba pochodzą od Vercela, ale Turbopack to bundler, czyli narzędzie składające kod aplikacji w pliki gotowe do uruchomienia w przeglądarce, a Turborepo zarządza zadaniami w całym repozytorium. Najwyżej w tej hierarchii stoją systemy budowania projektowane z myślą o ogromnych repozytoriach i wielu językach programowania. Przykładem jest Bazel, system budowania od Google. Daje bardzo precyzyjną kontrolę, ale wymaga sporej wiedzy i dyscypliny, więc w typowym projekcie webowym i mobilnym rzadko się opłaca.
Kiedy monorepo ma sens
Z naszego doświadczenia monorepo najlepiej sprawdza się wtedy, gdy kilka aplikacji jest częścią jednego produktu i zmienia się razem. Najczęstsze sygnały, że warto je rozważyć:
ten sam zespół rozwija aplikację webową, mobilną i backend jednego produktu,
aplikacje dzielą logikę biznesową, typy danych lub komponenty interfejsu,
większość funkcji wymaga zmian w kilku częściach systemu naraz,
wspólne biblioteki są dziś kopiowane między repozytoriami albo publikowane tylko po to, żeby zaraz je zaktualizować,
firma chce utrzymywać jeden design system dla kilku produktów.
Przykład z praktyki: aplikacja rezerwacyjna z wersją webową, aplikacją w React Native i backendem w NestJS. Wszystkie trzy korzystają z tych samych reguł walidacji, tych samych statusów rezerwacji i tych samych typów danych. Dodanie nowego pola w formularzu rezerwacji to w monorepo jedna zmiana, jeden przegląd kodu i jedno wdrożenie całości.
Mniej sensu ma tam, gdzie projekty są naprawdę niezależne. Kilka stron dla różnych klientów, które nic ze sobą nie dzielą, nie zyska na wspólnym repozytorium. Podobnie jest, gdy różne zespoły pracują w zupełnie innych technologiach, w innym rytmie i z innymi uprawnieniami. Monorepo nie naprawi też problemów organizacyjnych. Jeśli zespoły nie rozmawiają ze sobą, wspólne repozytorium szybciej wyciągnie konflikty na wierzch, ale ich nie rozwiąże.
Migracja nie musi oznaczać wielkiego przepisywania. Zwykle zaczynamy od wybrania dwóch części, które najwięcej ze sobą dzielą, na przykład aplikacji webowej i mobilnej, i przeniesienia ich do wspólnego repozytorium z zachowaniem historii zmian. Potem wydzielamy pierwszy współdzielony pakiet, najczęściej typy danych albo walidację. Równolegle trzeba przygotować proces CI/CD, który od początku buduje i testuje tylko to, co się zmieniło. Jeśli ten krok zostanie odłożony, zespół szybko zniechęci się do nowego układu. Kolejne aplikacje i biblioteki dochodzą stopniowo, gdy pierwsza część działa stabilnie.
W starszych projektach migracja bywa dobrą okazją do uporządkowania kodu, który przez lata rozrósł się w kilku kopiach. Nie warto jednak łączyć jej z dużym refaktoringiem. Problemy, jakie niesie kod legacy w starszych systemach, lepiej rozwiązywać krok po kroku, po przeniesieniu kodu, a nie w trakcie.
FAQ
FAQ - Najczęściej zadawane pytania o monorepo
Przeniesienie projektu do monorepo trwa zwykle od kilku dni do kilku tygodni, a w dużych systemach z wieloma zespołami może rozłożyć się na kilka miesięcy. Najprostszy przypadek to dwie aplikacje w tej samej technologii, na przykład webowa i mobilna w JavaScript, które przenosi się do wspólnego repozytorium z zachowaniem historii zmian i podstawową konfiguracją narzędzi. Więcej czasu zajmuje wydzielenie współdzielonych pakietów, ujednolicenie wersji bibliotek i przebudowa procesu CI/CD tak, żeby testował i wdrażał tylko zmienione części. Przykładowo projekt z aplikacją webową, mobilną i API w TypeScript można przenieść etapami, zaczynając od wspólnych typów danych, bez zatrzymywania prac nad nowymi funkcjami. Czas zależy od liczby repozytoriów, różnic w wersjach zależności, liczby osób pracujących równolegle i od tego, jak rozbudowany jest obecny proces wdrażania.
Tak, monorepo dobrze sprawdza się w małych zespołach, jeśli rozwijają one kilka powiązanych aplikacji jednego produktu. Kilka osób, które pracują jednocześnie nad stroną, aplikacją mobilną i backendem, najbardziej odczuwa koszt synchronizowania zmian między osobnymi repozytoriami, a jednocześnie nie potrzebuje zaawansowanej kontroli uprawnień. W małym projekcie zwykle wystarczą workspaces w menedżerze pakietów, bez rozbudowanych narzędzi. Przykładowo trzyosobowy zespół rozwijający aplikację rezerwacyjną może trzymać frontend, aplikację mobilną i API w jednym repozytorium i wprowadzać nową funkcję jedną zmianą. Opłacalność zależy od tego, ile kodu aplikacje ze sobą dzielą i jak często zmiany dotyczą kilku części naraz.
Tak, każda aplikacja w monorepo może mieć własny proces wdrożenia i własny harmonogram wydań. Wspólne repozytorium oznacza wspólne miejsce przechowywania kodu, a nie obowiązek wdrażania wszystkiego naraz. Proces CI/CD sprawdza, które części zostały zmienione, i buduje oraz wdraża tylko je, więc poprawka w aplikacji webowej nie wymaga nowej wersji aplikacji mobilnej. Przykładowo backend może być wdrażany kilka razy dziennie, a aplikacja mobilna trafiać do sklepów raz na dwa tygodnie, choć obie leżą w jednym repozytorium. Sposób konfiguracji zależy od używanych narzędzi, platformy, na której działa CI/CD, i od tego, jak wyraźnie oddzielono od siebie poszczególne aplikacje.
Tak, aplikację webową w Next.js i mobilną w React Native można trzymać w jednym monorepo i współdzielić między nimi część kodu. Najczęściej wspólne są typy danych, walidacja formularzy, logika komunikacji z API, tłumaczenia i reguły biznesowe, a osobne pozostają komponenty interfejsu, bo web i aplikacja mobilna korzystają z innych elementów wizualnych. Istnieją też biblioteki, które pozwalają pisać część komponentów raz dla obu platform. Przykładowo sklep internetowy i jego aplikacja mobilna mogą korzystać z tego samego modułu koszyka i tych samych zasad naliczania rabatów, co eliminuje rozbieżności cen między kanałami. Ilość współdzielonego kodu zależy od podobieństwa obu aplikacji, wersji bibliotek i tego, czy interfejs ma wyglądać tak samo na obu platformach.
Tak, dostęp do zmian w wybranych częściach monorepo można kontrolować, choć ograniczenie samego odczytu kodu jest trudniejsze niż przy osobnych repozytoriach. Platformy takie jak GitHub czy GitLab pozwalają wskazać właścicieli poszczególnych katalogów, bez których akceptacji zmiana nie zostanie scalona z główną gałęzią. Dzięki temu na przykład zmiany w module płatności muszą zatwierdzić osoby odpowiedzialne za ten obszar. Jeśli natomiast część kodu ma być w ogóle niewidoczna dla wybranych osób, na przykład zewnętrznego podwykonawcy, zwykle trzyma się ją w osobnym repozytorium i dołącza jako zależność. Wybór rozwiązania zależy od wymagań bezpieczeństwa, liczby zespołów z zewnątrz i tego, jak wrażliwy jest dany fragment systemu.
Tak, monorepo zwykle przyspiesza rozwój produktów złożonych z kilku powiązanych aplikacji, pod warunkiem że ma dobrze skonfigurowany proces budowania i testowania. Najwięcej czasu oszczędza się na zmianach dotyczących kilku części systemu naraz, które zamiast kilku osobnych zmian, przeglądów i wydań bibliotek wymagają jednej. Zyskuje się też na wspólnych narzędziach i konfiguracji, których nie trzeba utrzymywać w każdym repozytorium osobno. Przykładowo dodanie nowego statusu zamówienia, widocznego w panelu, aplikacji mobilnej i API, staje się jednym zadaniem zamiast trzech skoordynowanych. Efekt zależy od tego, ile kodu aplikacje faktycznie dzielą, czy CI uruchamia tylko to, co się zmieniło, i czy zespół pilnuje granic między modułami.
Tak, aplikację lub bibliotekę z monorepo można wydzielić do osobnego repozytorium, także z zachowaniem historii zmian dotyczących tylko jej katalogu. Git ma narzędzia, które pozwalają przepisać historię tak, żeby nowe repozytorium zawierało wyłącznie wybraną część kodu. Najwięcej pracy wymaga zastąpienie bezpośrednich odwołań do wspólnych pakietów opublikowanymi wersjami tych pakietów i odtworzenie procesu wdrożenia. Przykładowo firma, która sprzedaje jeden z modułów jako osobny produkt albo przekazuje go innemu zespołowi, może go wydzielić bez utraty historii. Pracochłonność zależy od tego, jak mocno wydzielana część jest powiązana z resztą kodu, dlatego wyraźne granice między modułami od początku znacząco ułatwiają taki krok.
Wybór między Monorepo a Multi-Repo to kluczowe decyzje architektoniczne w zarządzaniu projektami IT. Odpowiedni dobór może istotnie wpłynąć na efektywność pracy, jak i łatwość utrzymania projektu. Warto zatem rozeznać argumenty przemawiające za obiema opcjami, zanim podejmie się decyzję.
Mikroserwisy versus Monolit to dwie konkurencyjne podejścia do budowy systemów informatycznych. Obie architektury mają swoje atuty i słabości, a wybór pomiędzy nimi może być kluczowy dla sukcesu projektu. Celem tego artykułu jest dogłębne zrozumienie i porównanie tych dwóch podejść.
Czy możemy połączyć zalety monolitu i mikroserwisów? Wyjaśniamy koncepcję Modularnego Monolitu, nowoczesnego podejścia do projektowania aplikacji monolitycznych. Te praktyki pomagają zorganizować kod w łatwy do zrozumienia, skalowalny i łatwy do utrzymania sposób. Dowiedz się, jak zastosować tę koncepcję w swoim projekcie.
CI/CD to skrót od Continuous Integration i Continuous Delivery, czyli procesów ciągłej integracji i ciągłego dostarczania. Jest to metoda zarządzania projektem oprogramowania, która polega na ciągłym i automatycznym sprawdzaniu, testowaniu i wdrażaniu kodu do produkcji.
Rosnąca złożoność aplikacji webowych sprawia, że wydajność narzędzi developerskich ma dziś ogromne znaczenie. Turbopack, nowy bundler od Vercela, powstał jako odpowiedź na ograniczenia klasycznych rozwiązań, takich jak Webpack, szczególnie w dużych projektach Next.js. Jego głównym celem jest maksymalne skrócenie czasu startu aplikacji i natychmiastowy hot reload podczas pracy z kodem.
Bazel to jedno z najszybszych i najbardziej niezawodnych narzędzi do budowania projektów, stworzone z myślą o pracy na dużą skalę. Dzięki inteligentnemu zarządzaniu zależnościami i zaawansowanym mechanizmom cache’owania znacząco skraca czas kompilacji, nawet w bardzo rozbudowanych repozytoriach. Pozwala zespołom pracować szybciej, stabilniej i bardziej przewidywalnie, niezależnie od stosowanych języków programowania.