Baza pytań rekrutacyjnych i wiedzy. Filtruj, szukaj i sprawdzaj swoją wiedzę.
Architektura monolityczna to pojedyncza wdrażalna aplikacja, w której moduły działają w jednym procesie i zwykle współdzielą jedną bazę danych. System buduje się, testuje i wdraża jako całość, co jest proste na początku, ale może prowadzić do silnych powiązań przy wzroście.
Zalety: prostszy rozwój i wdrożenia, łatwe testowanie/debugowanie lokalne oraz silna spójność i proste transakcje. Wady: trudniej niezależnie skalować części systemu, wolniejsze cykle wdrożeń i rosnący kod może stać się silnie powiązany i trudniejszy do bezpiecznych zmian.
Utrzymuj jasne granice modułów (np. domenowe), egzekwuj zasady zależności, utrzymuj cienkie warstwy i dodawaj testy automatyczne. Stosuj wewnętrzne API, ograniczaj współdzielony stan i regularnie refaktoruj, aby monolit pozostał modułowy i łatwy w zmianach.
Monolit modułowy to wciąż jedna jednostka wdrożeniowa, ale z silną separacją modułów. Każdy moduł posiada własną domenę i komunikuje się przez dobrze zdefiniowane interfejsy, co poprawia utrzymanie i ułatwia przyszłe wydzielanie usług.
Preferuj migrację krokową (Strangler Fig): wydzielaj po jednej funkcji/domenie za stabilnym API i dodaj dobre logi/metryki/tracing. Unikaj big‑bang rewrite’u; rozdzielaj dane i logikę stopniowo.
Monolit to jedna jednostka wdrożeniowa (jedna aplikacja/serwis) zawierająca wiele modułów/funkcji. Często jest świetny na start, bo upraszcza development, testy i wdrożenia oraz ułatwia transakcje i debugowanie.
Gdy zespół jest mały, domena szybko się zmienia i chcesz szybko iterować przy prostych operacjach. Mikroserwisy zwykle opłacają się dopiero, gdy realnie potrzebujesz niezależnych wdrożeń/skalowania i masz jasne granice.
To wciąż jedna jednostka wdrożeniowa, ale z mocnymi granicami wewnętrznymi: moduły posiadają swoją domenę i komunikują się przez jasno zdefiniowane interfejsy. To poprawia utrzymanie i ułatwia przyszłe wydzielanie serwisów.
Ustal jasne granice modułów (często domenowe), egzekwuj zasady zależności i utrzymuj cienkie warstwy. Dodaj testy automatyczne i regularnie refaktoruj, żeby kod pozostał modułowy i łatwy do zmian.
Zrób aplikację stateless (sesje w Redis/DB), uruchom wiele instancji za load balancerem i osobno skaluj bazę (indeksy, cache, read repliki). Zwróć uwagę na współdzielone zasoby jak pliki i joby w tle.
Feature flagi pozwalają włączać/wyłączać funkcje w runtime bez wdrożenia. Ułatwiają bezpieczne release’y (dark launch, rollout) i szybki rollback, ale wymagają sprzątania, żeby nie mieć „flag debt”.
Dodaj warstwę routingu, wybierz małą funkcję/domenę, wydziel ją za stabilnym API i stopniowo przełączaj ruch. Powtarzaj kroki „po plasterku” z monitoringiem i rollbackiem, aż stara ścieżka będzie do usunięcia.
Własność danych i spójność: kto jest właścicielem których tabel i jak utrzymać spójność w trakcie przejścia (dual writes/outbox/eventy). Migracja powinna być krokowa z jasnym momentem cutover i strategią rollback.
Szukaj bounded contexts: funkcji z jasnym właścicielem, danymi i małą liczbą zależności. Zacznij od części, które często się zmieniają lub mają wyraźne potrzeby skalowania, i nie dziel na początku mocno sprzężonych fragmentów.
To system podzielony na serwisy, ale nadal mocno sprzężony (wspólna baza, synchroniczne „gadatliwe” wywołania, skoordynowane wdrożenia). Unikasz przez jasną własność, asynchroniczność tam gdzie trzeba, stabilne kontrakty i niezależne wdrożenia.
Monorepo to strategia repozytorium (wiele projektów w jednym repo). Monolit to jednostka wdrożeniowa/runtime (jedna aplikacja). Możesz mieć monolit w monorepo albo mikroserwisy w monorepo.
To kod bez jasnych granic i z dużą, przypadkową zależnością między częściami. Objawy: brak jasnej odpowiedzialności, losowe regresje po zmianach, dużo globalnego stanu i „wszystko zależy od wszystkiego”.
Organizuj kod po funkcjach/domenach, zdefiniuj jasne publiczne API między modułami i ogranicz zależności (np. reguły pakietów, testy architektury). Trzymaj wspólne utilsy małe i unikaj „god modułów”.
Zastosuj expand/contract: najpierw dodaj nowy schemat (np. nullable kolumna/nowa tabela) i wdroż kod obsługujący stare i nowe; potem zmigruj dane; a na końcu usuń stare w kolejnej wersji. To minimalizuje downtime i wspiera rollback.
Skaluj poziomo (wiele stateless instancji za load balancerem) i przenieś ciężkie zadania do asynchronicznych jobów/kolejek (background processing). Dodatkowo skaluj odczyty przez cache i repliki.
Package-by-layer grupuje kod po warstwach technicznych (kontrolery/serwisy/repo). Package-by-feature grupuje kod po funkcji/domenie. Struktura feature-based często lepiej skaluje, bo powiązany kod jest razem i granice są czytelniejsze.
Użyj osobnego procesu workera (ta sama baza kodu, inny entrypoint) konsumującego kolejkę, z retry i idempotencją. To nie blokuje requestów web i daje lepszą kontrolę współbieżności oraz błędów.
Trzymaj utilsy blisko funkcji, która jest ich właścicielem, a wspólny kod wydzielaj dopiero, gdy realnie potrzebuje go wiele modułów. Preferuj małe, nazwane biblioteki ze znanym właścicielem zamiast jednego ogromnego `utils`.
Użyj cache (zależności i artefaktów builda), uruchamiaj testy/linty inkrementalnie dla zmienionych modułów i równoleglij joby. Pomaga też niezależność modułów, żeby nie przebudowywać wszystkiego po małej zmianie.
Rób to krokowo: wybierz jedną granicę, dodaj testy wokół zachowania, refaktoruj za stabilnym interfejsem i wypuszczaj małe kroki. Planuj czas na dług techniczny i unikaj big-bang rewrite’u.
Single deployable oznacza, że wdrażasz jeden artefakt jako jedną całość (jedna wersja do zbudowania, przetestowania i wdrożenia). To upraszcza release’y i rollbacki oraz unika niedopasowania wersji między serwisami. Minusem jest większy blast radius, gdy coś pójdzie źle.
Trzymaj piramidę testów: dużo szybkich unit testów, mniej integracyjnych i mało E2E. W integracji testuj kluczowe “seamy” (DB, messaging) z realistycznymi zależnościami (np. Testcontainers) i dbaj o równoległość oraz stabilność. Unikaj jednego gigantycznego test-suite “testuje wszystko”.
Organizuj kod po funkcjach/domenach (a nie tylko po warstwach technicznych), wystawiaj małe wewnętrzne API między modułami i zabraniaj “sięgania” do wnętrza innych modułów importami. Dodaj ownership (kto utrzymuje co) i checki architektoniczne (granice modułów), żeby granice nie rozjeżdżały się w czasie.
Typowe opcje: osobna baza per tenant (mocna izolacja, większy koszt), osobna schema per tenant (dobra izolacja, średnia złożoność) albo współdzielone tabele z `tenant_id` (najtańsze, najtrudniejsze do poprawnego egzekwowania). Niezależnie od podejścia musisz wszędzie wymuszać tenant scoping oraz dodać właściwe indeksy i checki bezpieczeństwa.
Zdefiniuj SLI wydajności (np. p95) i monitoruj je cały czas. Dodaj profiling/tracing dla wolnych endpointów, rób load testy krytycznych flow i ustaw budżety/alerty, żeby łapać regresje wcześnie. Feature flagi pomagają szybko wycofać zmianę, gdy trzeba.
Structured logging oznacza, że logi mają stałe pola (np. JSON) typu `level`, `message`, `requestId`, `userId`. Pomaga, bo możesz je łatwo filtrować, wyszukiwać i łączyć między wieloma fragmentami kodu bez parsowania „losowego” tekstu.
Correlation ID (request ID) to unikalny identyfikator requestu, który trafia do logów. Generuj go na wejściu (middleware/filter HTTP) albo przyjmij z upstreamu, a potem przekazuj przez wszystkie warstwy oraz joby uruchomione przez ten request.
Zdefiniuj granice modułów i reguły kierunku zależności (np. moduły funkcjonalne mogą zależeć od shared kernel, ale nie od siebie nawzajem). Wymuszaj to buildem (osobne moduły Gradle/Maven), testami architektury oraz wystawianiem stabilnych interfejsów/fasad zamiast sięgania do „wnętrzności”.
Pomagają rozsprzęgać moduły: jeden moduł publikuje event („OrderPlaced”), a inne reagują bez ścisłych zależności. Pułapki: decyzja sync vs async, niewrzucanie ciężkiej pracy do tej samej transakcji oraz niezawodność i idempotencja handlerów (event może się powtórzyć).
Traktuj tabele jako „własność” modułów: tylko właściciel zapisuje dane i wystawia dostęp przez swoje API/fasadę. Unikaj cross-module joinów „gdzie popadnie”; zamiast tego pobieraj dane przez moduł właściciela albo użyj domain events. Jeśli trzeba, wymuszaj to osobnymi schematami, granicami repozytoriów oraz code review/regułami architektury.