Wzorzec singleton pattern bywa prosty do opisania, ale w praktyce ma więcej konsekwencji niż widać na pierwszy rzut oka. Chodzi o sytuację, w której jedna klasa albo jeden obiekt ma istnieć tylko raz i ma być dostępny z wielu miejsc aplikacji. Ja traktuję go przede wszystkim jako narzędzie do kontroli współdzielonego zasobu, a nie jako domyślny sposób budowania każdego serwisu czy managera.
Jedna instancja, wspólny punkt dostępu i sporo kompromisów
- Singleton ogranicza tworzenie obiektu do jednej instancji i daje globalny punkt dostępu.
- Najczęściej ma sens dla rzeczy współdzielonych: loggera, konfiguracji, klienta flag, cache albo warstwy dostępu do zasobu.
- W nowoczesnym JavaScript bardzo często prostszy jest stan modułu niż klasa singletonowa.
- Główne ryzyko to ukryty stan, trudniejsze testy i mocniejsze sprzężenie między częściami aplikacji.
- Jeśli jedna instancja nie ma twardego uzasadnienia technicznego, zwykle lepsze są jawne zależności.
Czym jest singleton i kiedy ma sens
Najkrócej: chodzi o obiekt, którego nie tworzysz wielokrotnie, tylko pobierasz zawsze z tego samego miejsca. Taki wzorzec jest przydatny wtedy, gdy wiele fragmentów aplikacji musi korzystać z tego samego stanu albo z tego samego zasobu, a druga kopia nic nie poprawi, tylko wprowadzi chaos. W projektach webowych widzę to najczęściej przy loggerach, konfiguracji, cache, flagach funkcji i klientach odpowiedzialnych za integrację z jednym usługodawcą.
Jedna instancja ma sens tylko wtedy, gdy naprawdę współdzielisz coś kosztownego, wrażliwego albo wymagającego spójności. Jeśli obiekt jest zwykłym elementem domeny, np. zamówieniem, użytkownikiem albo koszykiem, singleton zwykle jest błędem projektowym, bo miesza odpowiedzialności i ukrywa zależność tam, gdzie powinna być widoczna.
Gdzie jedna instancja faktycznie pomaga
- Przy loggerze, który ma jeden format, jeden poziom logowania i jeden kanał wyjścia.
- Przy konfiguracji aplikacji, którą odczytuje wiele modułów, ale nie powinno się jej duplikować.
- Przy kliencie feature flag lub analityki, który ma utrzymywać spójny stan połączenia.
- Przy warstwie buforującej, jeśli cache ma być wspólny dla całego procesu.
Przeczytaj również: Maszyna Stanów w Springu - Kiedy Warto, Jak Budować i Unikać Błędów?
Kiedy to tylko globalny stan w ładniejszym opakowaniu
Jeśli z obiektu zaczyna korzystać wszystko, ale nie ma jasnego powodu, dlaczego tylko jedna kopia ma istnieć, to zwykle nie projektujesz wzorca, tylko globalny stan. To ważne rozróżnienie, bo globalny obiekt jest łatwy na starcie, ale z czasem robi się trudny do testowania, trudny do podmiany i zaskakująco kruchy przy refaktoryzacji. Gdy rozumiesz już ten punkt, łatwiej zobaczyć, jak singleton działa technicznie i gdzie ukrywa się jego cena.

Jak działa ten wzorzec pod spodem
Mechanika jest prosta, ale warto ją rozłożyć na części, bo wtedy od razu widać, czemu ten wzorzec tak często budzi zastrzeżenia. Singleton zwykle opiera się na trzech rzeczach: prywatnym miejscu na instancję, publicznej metodzie, która ją zwraca, oraz zabezpieczeniu, które blokuje tworzenie kolejnych egzemplarzy. W językach z prawdziwie prywatnym konstruktorem da się to wymusić mocniej; w JavaScript częściej chodzi o kontrolę konwencją i prywatnym polem statycznym.
- Przechowywanie instancji - klasa lub moduł trzyma jedno pole, w którym zapisany jest jedyny egzemplarz.
- Lazy initialization - obiekt powstaje dopiero przy pierwszym użyciu, a nie od razu przy starcie aplikacji.
- Statyczny punkt dostępu - reszta kodu wywołuje jedną metodę, zamiast tworzyć nowe obiekty ręcznie.
- Blokada przed duplikacją - dodatkowe wywołania zwracają istniejącą instancję albo kończą się błędem.
W aplikacjach wielowątkowych dochodzi jeszcze kwestia synchronizacji. Dwie równoległe ścieżki nie mogą stworzyć dwóch egzemplarzy w tym samym momencie, więc potrzebna jest blokada, atomowe sprawdzenie albo inny mechanizm bezpiecznej inicjalizacji. W JavaScript na froncie problem jest zwykle mniejszy, ale w backendzie, workerach i innych środowiskach z równoległością nie można tego zignorować. Na tym tle najczytelniej widać kod, który robi dokładnie to, co trzeba, i nic więcej.
Jak napisać go w JavaScript bez nadmiaru magii
Jeśli potrzebujesz klasy, najprostszy wariant wygląda tak: jedna prywatna zmienna statyczna pilnuje instancji, a statyczna metoda zwraca ten sam obiekt za każdym razem. Ja lubię ten zapis tylko wtedy, gdy faktycznie chcę modelować zachowanie obiektu, a nie tylko współdzielić dane. W przeciwnym razie dokładanie klasy dla samej idei singletona robi więcej szumu niż pożytku.
class Logger {
static #instance = null;
constructor() {
if (Logger.#instance) {
return Logger.#instance;
}
this.prefix = "[app]";
Logger.#instance = this;
}
static getInstance() {
if (!Logger.#instance) {
Logger.#instance = new Logger();
}
return Logger.#instance;
}
info(message) {
console.log(this.prefix, message);
}
}Ten wariant działa tak: pierwsze wywołanie tworzy obiekt, a każde kolejne dostaje już istniejący egzemplarz. Dzięki temu logika inicjalizacji jest w jednym miejscu, a reszta aplikacji nie musi pamiętać, czy tworzy nowy obiekt, czy tylko pobiera stary. Warto jednak pamiętać, że w JavaScript często jeszcze lepszy efekt daje zwykły moduł, bo sam import już zapewnia wspólny stan bez dodatkowej ceremonii. Jeśli zależy ci tylko na współdzieleniu danych, to właśnie tam najczęściej wygrywa prostota.
Przy użyciu w kodzie dbam o jedną rzecz szczególnie mocno: nie chcę, żeby singleton stał się ukrytym miejscem na wszystko. Jeśli po kilku tygodniach zaczyna przechowywać konfigurację, cache, liczniki i jeszcze logikę biznesową, to znaczy, że obiekt urósł ponad swoją rolę. Wtedy problemem nie jest już sam wzorzec, tylko to, że stał się magazynem przypadkowych odpowiedzialności. Stąd naturalnie przechodzę do pytania, gdzie taki wybór pomaga, a gdzie zaczyna przeszkadzać.
Gdzie singleton pomaga, a gdzie szkodzi
Najlepsze zastosowania są dość nudne i właśnie dlatego bezpieczne: jeden logger, jeden klient feature flag, jedna bramka do wspólnego cache albo jeden menedżer konfiguracji. To są sytuacje, w których koszt powielenia obiektu jest realny albo jego duplikacja grozi niespójnością. W projektach webowych dobrze działa to też przy komponencie, który inicjalizuje zewnętrzną usługę raz i potem tylko udostępnia jej stan kolejnym częściom aplikacji.
- Pomaga, gdy potrzebujesz jednego punktu kontroli nad zasobem współdzielonym.
- Pomaga, gdy inicjalizacja jest kosztowna i nie chcesz jej powtarzać.
- Pomaga, gdy różne moduły mają odczytywać ten sam stan, ale nie powinny go redefiniować.
- Szkodzi, gdy obiekt zaczyna ukrywać zależności zamiast je ujawniać.
- Szkodzi, gdy stan ma być różny dla użytkownika, żądania, testu albo środowiska.
- Szkodzi, gdy chcesz łatwo podmieniać implementację albo tworzyć wiele niezależnych egzemplarzy.
Największy problem pojawia się zwykle nie na etapie pisania, tylko później, kiedy ktoś próbuje to przetestować albo rozbudować. Ukryty stan potrafi przeciekać między testami, a funkcje, które sięgają po singleton głęboko w środku, trudniej przenieść do innego kontekstu. Jeśli projekt rośnie, to właśnie ten koszt robi się najbardziej odczuwalny. Dlatego porównanie z innymi sposobami zarządzania stanem jest tu bardziej praktyczne niż sama definicja.
Singleton, moduł i jawne zależności to nie to samo
W JavaScript część osób wrzuca do jednego worka singleton, stan modułu i globalne obiekty. Ja tego nie robię, bo różnice są istotne dla testów, architektury i utrzymania kodu. Poniższa tabela pokazuje to bez ozdobników.
| Rozwiązanie | Co daje | Co komplikuje | Kiedy wygrywa |
|---|---|---|---|
| Singleton klasowy | Jedną instancję, kontrolę dostępu i możliwość ukrycia stanu za metodą | Testy, sprzężenie i dodatkową warstwę kodu | Gdy naprawdę potrzebujesz klasy i jednego obiektu |
| Stan modułu | Jedną kopię danych bez konstruktorów i bez dodatkowego ceremoniału | Trzeba pilnować, żeby moduł nie urósł do śmieciowego magazynu | Gdy w JS wystarczy współdzielony serwis lub cache |
| Globalny obiekt | Najłatwiejszy dostęp z każdego miejsca | Brak kontroli, łatwe nadpisanie i słaba przewidywalność | Prawie nigdy, chyba że świadomie budujesz bardzo niski poziom abstrakcji |
| Jawne wstrzykiwanie zależności | Widoczne zależności, lepszą testowalność i łatwiejszą podmianę implementacji | Trochę więcej kodu przy składaniu obiektów | Gdy zależność ma być elastyczna i łatwa do mockowania |
W praktyce najczęściej wygrywa stan modułu albo jawne wstrzykiwanie zależności. Moduł jest prosty, bo sam system importów gwarantuje jedną kopię eksportowanego stanu, a zależności jawne są po prostu czytelniejsze dla zespołu i testów. Singleton ma sens wtedy, gdy potrzebujesz właśnie klasy z kontrolowaną instancją, a nie tylko jednego miejsca na dane. To prowadzi do ostatniego kroku: kilku pytań, które warto zadać, zanim w ogóle wdrożysz ten wzorzec.
Zanim wpuścisz singleton do projektu, sprawdź te granice
- Czy ta rzecz naprawdę musi istnieć tylko raz, czy po prostu wygodniej byłoby trzymać ją globalnie?
- Czy stan ma być wspólny dla całej aplikacji, czy jednak powinien się różnić dla żądania, użytkownika albo testu?
- Czy można przekazać obiekt jako zależność zamiast pobierać go z ukrytego miejsca?
- Czy ktoś będzie musiał to łatwo podmienić, zasymulować albo zresetować w testach?
- Czy zespół zrozumie ten wybór po miesiącu, czy będzie musiał zgadywać, skąd bierze się stan?
Jeśli na pierwsze dwa pytania nie odpowiadasz bez wahania „tak”, ja najczęściej odpuszczam singleton i wybieram moduł albo jawne wstrzykiwanie zależności. To są rozwiązania mniej efektowne, ale zwykle bardziej przewidywalne, lepiej testowalne i po prostu zdrowsze dla kodu, który ma żyć dłużej niż jeden sprint. Singleton zostawiam dla sytuacji, w których jedna instancja naprawdę ma sens techniczny i biznesowy, a nie tylko dobrze wygląda w diagramie.