Dwa sklepy internetowe. W obu da się przejrzeć produkty, dodać je do koszyka, zapłacić kartą i sprawdzić status zamówienia. Oba wyglądają profesjonalnie i działają szybko, a klient końcowy raczej nie zauważy między nimi różnicy. Jeden kosztował 5 tys. zł, drugi 50 tys. zł.
Kwoty są umowne. Sklep uruchomiony tanio na dojrzałej platformie potrafi być niezawodny, a drogi system pisany na zamówienie potrafi mieć poważne błędy. Różnica, o której chcę napisać, dotyczy czego innego: implementacja poprawnego scenariusza zakupowego to jeden zakres pracy, a zaprojektowanie systemu, który zachowuje się przewidywalnie również wtedy, gdy ten scenariusz zawiedzie, to zakres znacznie większy.
Przykłady pochodzą z Artovnii, działającego marketplace'u opartego na Medusa.js, który sam rozwijam i utrzymuję. Pokazuję w nich fragment rzeczywistej implementacji, pomiary z diagnostyki produkcyjnej i wyniki testów obciążeniowych, a nie wyobrażenie o tym, jak taki system mógłby działać.
Happy path, czyli kiedy wszystko działa
Większość implementacji zaczyna się od happy path. Klient dodaje produkt do koszyka, przechodzi do płatności, operator potwierdza transakcję, backend tworzy zamówienie, aktualizuje stan magazynowy i wysyła potwierdzenie. Ten scenariusz najłatwiej zaimplementować, przetestować i pokazać klientowi na demie.
Produkcja dokłada warunki, których na demie nie widać. Żądania HTTP kończą się timeoutem, mimo że serwer zdążył już wykonać operację. Webhooki przychodzą kilkukrotnie albo w innej kolejności. Dwie osoby jednocześnie kupują ostatnią dostępną sztukę. Worker przestaje działać w połowie zadania. Backend traci na chwilę połączenie z PostgreSQL lub Redisem, jedna z replik przestaje odpowiadać, zewnętrzny dostawca ma awarię. Endpoint, który odpowiada w 100 ms przy kilku użytkownikach, przy większym ruchu odpowiada w kilka sekund, a baza, która radziła sobie na starcie, staje się najwolniejszym elementem aplikacji.
To normalne zachowanie systemów działających w internecie. Kosztuje dopiero moment, w którym aplikacja musi wiedzieć, co w takiej sytuacji zrobić.
Gdy serwery przestają się ze sobą komunikować
W prostym sklepie wystarczy wyobrazić sobie jeden serwer, który przyjmuje żądania, rozmawia z bazą i zwraca odpowiedzi. W praktyce frontend komunikuje się z API, API korzysta z PostgreSQL i Redisa, osobne workery przetwarzają zadania w tle, a płatności, dostawy, powiadomienia czy fakturowanie mogą wymagać komunikacji z zewnętrznymi usługami. Każde z tych połączeń może zawieść.
Backend zapisuje zmianę zamówienia w bazie i próbuje powiadomić komponent, który ma wykonać kolejny krok. Komunikacja zostaje przerwana. Zamówienie jest zaktualizowane, ale dalszy proces nie dostał informacji, a jeśli aplikacja powtórzy całą operację, część zmian może wykonać drugi raz. Podobnie wygląda sytuacja z samą bazą: serwer wysyła polecenie zatwierdzenia transakcji i traci połączenie przed otrzymaniem odpowiedzi. Nie wiadomo, czy transakcja została zatwierdzona, bo błąd komunikacji nie oznacza, że operacja się nie wykonała.
Dlatego try/catch i ponowienie żądania nie wystarczają. Trzeba rozróżniać operacje, które można bezpiecznie powtórzyć, od takich, których wynik trzeba najpierw ustalić. W Artovnii wykorzystuję między innymi trwałe rekordy operacji finansowych, klucze pozwalające wykrywać duplikaty oraz blokady współdzielone pomiędzy replikami backendu. Inne mechanizmy odpowiadają za ponawianie nieudanych zadań i kontrolowanie ich stanu.
Rozdzielenie API i workerów ogranicza wpływ długotrwałych zadań na obsługę żądań użytkowników, choć procesy nadal mogą konkurować o współdzieloną bazę danych, Redis i inne zasoby. Awarie sieci nadal się zdarzają, ale w krytycznych procesach finansowych i wybranych operacjach asynchronicznych Artovnia przechowuje stan pozwalający ustalić wynik przerwanej operacji i podjąć dalsze działania.
Zamówienie zapisane. A co z resztą zadań?
Opłacenie zamówienia nie kończy pracy backendu. Trzeba jeszcze wysłać potwierdzenie zakupu, obsłużyć powiadomienia o wysyłce i przekazać informacje do zewnętrznych usług, a nie ma powodu trzymać klienta na ekranie płatności, dopóki wszystkie te operacje się nie zakończą.
Do takich zadań służą mechanizmy asynchroniczne. Medusa.js ma Event Bus, który przekazuje zdarzenia do przetwarzania w tle. W Artovnii działa implementacja oparta na Redis i BullMQ, skonfigurowana na trzy próby wykonania nieudanego zadania z rosnącym opóźnieniem, liczącym od dwóch sekund.
Po wyczerpaniu prób BullMQ może zachować zadanie w stanie failed, co samo w sobie nie oznacza, że ktoś o problemie wie, ustali przyczynę i bezpiecznie wznowi operację. Stąd pytania o Dead Letter Queue, monitorowanie nieudanych zadań i kontrolowane ponawianie.
W Artovnii rozbudowałem pod tym kątem obsługę powiadomień e-mail. Administrator ma listę wiadomości wraz ze statusami pending, success i failure, może sprawdzić historię prób powiązanych z pierwotną wiadomością i ręcznie ponowić nieudaną wysyłkę. Dane nie są czytane z Redisa ani odpytywane u dostawcy na żywo. Panel korzysta z własnego endpointu admina, który przez standardowy Notification Module Medusy odczytuje rekordy z tabeli notification w PostgreSQL, zawężone do kanału e-mail i odświeżane co 30 sekund.
Samo ponowienie też wymaga zabezpieczeń. Korzysta z blokady PostgreSQL, piętnastominutowego cooldownu między próbami i klucza idempotencji przekazywanego do dostawcy wiadomości, żeby próba rozwiązania jednego problemu nie tworzyła kolejnego w postaci kilku identycznych maili u klienta. Status success oznacza przy tym przyjęcie wiadomości do wysłania zapisane lokalnie, a nie potwierdzenie, że trafiła do skrzynki odbiorcy.
To rozwiązanie dotyczy powiadomień e-mail, nie jest centralnym DLQ dla całego sklepu, a standardowe nieudane zadania Event Busa pozostają osobnym zagadnieniem. Pokazuje jednak różnicę między posiadaniem gotowej funkcjonalności a możliwością jej utrzymania. Samo retry nie wystarczy, trzeba jeszcze wiedzieć, co stało się po jego wyczerpaniu i jak bezpiecznie kontynuować proces.
Operator przyjął płatność, backend nie dostał odpowiedzi
Klient rozpoczyna płatność, backend wysyła do operatora żądanie utworzenia sesji Checkout, operator je przetwarza, po czym połączenie zostaje przerwane i odpowiedź nie dociera. Najprostsza implementacja spróbuje ponownie, a skoro każda próba tworzy nowe żądanie z nowym identyfikatorem, operator może utworzyć drugą, niezależną sesję. Nie prowadzi to automatycznie do podwójnego obciążenia klienta, ale system ma wtedy kilka prób płatności i musi wiedzieć, która z nich odpowiada aktualnemu koszykowi.
W Artovnii backend zapisuje dokładne parametry żądania oraz klucz idempotencji, zanim wywoła API operatora. W ramach okna idempotencji obsługiwanego przez operatora ponowienie z tym samym kluczem i parametrami ogranicza ryzyko utworzenia drugiej niezależnej operacji. Dostawcy ograniczają jednak czas przechowywania informacji o takich żądaniach, więc trzeba zdecydować, jak długo można ponawiać automatycznie i co zrobić później. Po upływie bezpiecznego okna Artovnia nie wysyła kolejnych żądań, tylko traktuje przypadek jako niejednoznaczny i wymagający ustalenia rzeczywistego stanu.
Podczas normalnego zakupu ta logika nigdy się nie uruchomi i na demie nie będzie jej widać.
Jedna płatność, dwa serwery, potencjalnie dwa zamówienia
Po opłaceniu koszyka klient wraca ze strony operatora do sklepu, a operator w tym samym czasie wysyła webhook o pomyślnej transakcji. Oba procesy mogą próbować sfinalizować ten sam koszyk, a przy kilku replikach backendu mogą trafić na różne serwery. Serwer A sprawdza, czy zamówienie istnieje, i nie znajduje go. Serwer B wykonuje to samo sprawdzenie z tym samym skutkiem. Oba zaczynają tworzyć zamówienie.
Nie chodzi o błędny warunek if. Dwie operacje poprawne z osobna dają niepoprawny wynik, gdy wykonują się równocześnie. To klasyczny race condition, a jego biznesowy koszt to zdublowane zamówienia, podwójna rezerwacja towaru i potencjalne problemy z rozliczeniem płatności.

Kliknij, aby powiększyć
Dwa równoległe procesy mogą próbować sfinalizować ten sam koszyk. Współdzielona blokada PostgreSQL pozwala koordynować ich wykonanie między replikami backendu.
W kodzie Artovnii wygląda to tak:
const result = await withCartPaymentLock(req.scope, id, async () => {
// This check must happen after acquiring the distributed lock. Otherwise
// a browser return and a Stripe webhook can both observe no order set and
// start the completion workflow on different API replicas.
const existingOrderSetId = await findOrderSetIdForCart(query, id)
if (existingOrderSetId) {
logger.debug?.(
`[POST complete] Cart ${id} already completed as order set ${existingOrderSetId}`
)
return { id: existingOrderSetId }
}
await assertCartHasNoCheckoutRefund(req.scope, id)
await ensureLoyaltyAdjustmentsOnCart(req.scope, id)
await ensureNewsletterDiscountAdjustmentsOnCart(req.scope, id)
return withValidatedCartDiscounts(req.scope, id, async () => {
const completion = await splitAndCompleteCartWorkflow(req.scope).run({
input: { id }
})
return completion.result
})
})Kluczowe jest tutaj nie samo sprawdzenie, czy zamówienie już istnieje, ale moment wykonania tego sprawdzenia. W Artovnii odbywa się ono dopiero po uzyskaniu blokady współdzielonej pomiędzy replikami backendu. Jeżeli pierwszy proces zdążył już zakończyć koszyk, drugi odnajduje istniejący wynik zamiast ponownie tworzyć zamówienia.
Mechanizm blokad dostarcza moduł lockingu Medusa.js oparty na PostgreSQL, natomiast sposób ich wykorzystania oraz dodatkowe sprawdzenia wynikają z architektury Artovnii. Obie ścieżki, powrót z płatności i webhook, muszą używać tego samego klucza blokady, a oczekiwanie na nią ma swój limit czasu. Jeśli pierwsza próba finalizacji się nie powiedzie, kolejne żądanie może podjąć finalizację, o ile samo nie zakończy się wcześniej timeoutem. Dlatego poza blokadą potrzebna jest jeszcze weryfikacja, czy dana sesja płatności nie została już wykorzystana do utworzenia zamówień.
Refund rzadko jest jednym wywołaniem API
Z zewnątrz zwrot wygląda prosto. Administrator klika „Zwróć środki”, backend wysyła żądanie do operatora i aktualizuje status zamówienia.
Teraz załóżmy, że operator wykonał zwrot, ale backend przestał działać, zanim zapisał tę informację w bazie. Wynik operacji pozostaje nieznany, a ponowienie bez sprawdzenia może oznaczać drugi zwrot tych samych środków. Do tego dochodzą inne przypadki: dwóch administratorów uruchamia refund jednocześnie, worker ponawia przerwaną operację, a w marketplace w tym samym czasie może trwać wypłata środków dla sprzedawcy.
W Artovnii refund jest osobną, trwałą operacją finansową. Zanim nastąpi wywołanie operatora, system tworzy rekord z unikalnym identyfikatorem i rezerwuje odpowiednią część dostępnego salda. Transakcje PostgreSQL i blokady finansowe zabezpieczają dostęp do tych samych środków, a przy ponownym użyciu identyfikatora system weryfikuje, czy nie został on użyty z innymi parametrami. Wynik od operatora to osobny etap i dopiero jego potwierdzenie pozwala zmienić lokalny stan finansowy. Jeżeli wykonanie zostanie przerwane, proces reconciliation dla tej konkretnej operacji ustala wynik i dokańcza lub wznawia zwrot.
Retry odpowiada na pytanie, czy można spróbować jeszcze raz. Reconciliation odpowiada na pytanie, co wydarzyło się podczas poprzedniej próby, i w operacjach finansowych ta druga odpowiedź zwykle waży więcej.
W repozytorium Artovnii są testy obejmujące równoległe rezerwacje salda, awarię po sukcesie operatora, wygasłe blokady i konflikt refundu z wypłatą. Korzystają z rzeczywistego PostgreSQL, a operator płatności jest symulowany. Nie eliminuje to każdej możliwej awarii produkcyjnej, pokazuje jednak, że takie sytuacje były projektowane i sprawdzane. Dlaczego rozliczenia w marketplace działają inaczej niż w zwykłym sklepie, opisałem w tekście Płatności w Medusa.js dla sklepu i marketplace to dwa zupełnie różne światy.
Ostatnia sztuka produktu
W sklepie został jeden egzemplarz, a dwóch klientów niemal równocześnie przechodzi do kasy. Oba żądania sprawdzają dostępność i dostają informację, że produkt można kupić. Bez zabezpieczeń między sprawdzeniem a rezerwacją zapasu oba procesy spróbują sprzedać tę samą sztukę. Dodanie kolejnego sprawdzenia ilości niczego nie zmienia, bo problemem jest współbieżność, rezerwacje magazynowe i kolejność operacji.
W Artovnii dochodzi do tego synchronizacja stanów i możliwość zmiany powiązań między wariantami produktów a pozycjami inventory. Przed finalną rezerwacją system uzyskuje blokady, ponownie odczytuje aktualne powiązania i korzysta z mechanizmów rezerwacji Medusa Inventory.
Nie znaczy to, że każdy konflikt zostaje wyeliminowany przed płatnością. W obecnej architekturze wstępna walidacja dostępności następuje wcześniej, a właściwa rezerwacja magazynowa dopiero po pobraniu płatności, więc możliwy jest scenariusz, w którym klient zapłaci, a rezerwacja się nie powiedzie. Dla rozpoznanego braku zapasu Artovnia ma osobny mechanizm refundu awaryjnego. Projektowanie odpornego systemu polega na określeniu, których sytuacji da się uniknąć, gdzie zostaje ryzyko i co system robi, kiedy proces nie kończy się zgodnie z planem.
Wydajność: endpointy i baza danych
System może być logicznie poprawny i stać się bezużyteczny przy większym ruchu. Endpoint zwracający listę produktów działa błyskawicznie na środowisku developerskim i przy kilku użytkownikach. Potem rusza kampania reklamowa i wiele osób naraz przegląda tę samą kategorię. Każde żądanie przetwarza filtry, pobiera produkty, warianty, ceny, dostępność i powiązane dane. Jeśli endpoint wykonuje zbyt wiele zapytań, pobiera niepotrzebne relacje albo przetwarza duże ilości danych w pamięci, obciążenie rośnie wraz z liczbą żądań, a za nim czasy odpowiedzi, kolejki requestów i timeouty. Problem jednego endpointu potrafi wpłynąć na resztę aplikacji, zwłaszcza gdy współdzielą proces albo pulę połączeń z bazą.
Druga połowa tego problemu siedzi w bazie danych, o której łatwo zapomnieć przy optymalizowaniu obrazów i Core Web Vitals. Przy niewielkim ruchu nieefektywne zapytania pozostają niezauważone, a wraz ze wzrostem liczby rekordów zaczynają skanować większą część tabeli. Źle dobrany indeks, kosztowny JOIN, nieefektywny plan wykonania albo problem N+1 przy pobieraniu wariantów i cen potrafią wielokrotnie wydłużyć czas odpowiedzi. Jeśli endpointy i workery zajmą wszystkie dostępne połączenia, kolejne operacje czekają, nawet gdy same zapytania są tanie, a długie transakcje i blokady opóźniają operacje pozornie ze sobą niezwiązane.
Zdarza się też, że problem nie leży bezpośrednio w kodzie aplikacji. W Artovnii operacje koszyka potrafiły trwać 2,4-3,7 sekundy, a przyczyną okazała się kosztowna kompilacja JIT pojedynczego zapytania PostgreSQL, uruchamiana przez niekorzystny plan wykonania. Po wyłączeniu JIT czas wykonania tego zapytania spadł z ponad dwóch sekund do około 1,5 ms. Znalezienie przyczyny wymagało jednak instrumentacji backendu i analizy zapytań SQL, a nie dokładania zasobów serwera. Cały proces diagnostyki opisałem w osobnym artykule o problemie PostgreSQL JIT w Medusa.js.
Przy analizowaniu wydajności nie wystarczy też patrzeć wyłącznie na średni czas odpowiedzi. Podczas testów obciążeniowych Artovnii porównałem komunikację z PostgreSQL przez publiczne i prywatne połączenie sieciowe. Każdy wariant sprawdzałem narzędziem k6 na stronie produktu, przy 100 wirtualnych użytkownikach, w przebiegach po 5 minut i 30 sekund, co dawało około 10 700 żądań na przebieg i trzy przebiegi na wariant.
W obu wariantach mediana czasu odpowiedzi była niemal identyczna, około 72-73 ms. Znacznie większą różnicę pokazał p99, czyli czas, poniżej którego mieści się około 99% zmierzonych żądań. W badanych przebiegach zmniejszył się z 550,51 ms do 223,35 ms, a najwolniejsze pojedyncze żądanie z 1,93 s do 594 ms.

Kliknij, aby powiększyć
Porównanie czasów odpowiedzi p95 i p99 podczas testów k6 Artovnii. Mediana pozostała praktycznie bez zmian, natomiast najwolniejsze żądania osiągały niższe czasy w wariancie prywatnego połączenia.
To pokazuje, dlaczego wydajność warto oceniać również na podstawie najwolniejszych żądań. Dwie konfiguracje mogą mieć niemal identyczną medianę, a jednocześnie znacznie różnić się zachowaniem na końcu rozkładu opóźnień. Pomiar dotyczy porównywanych wariantów infrastruktury w tych konkretnych przebiegach i nie jest obietnicą wydajności przy dowolnym obciążeniu produkcyjnym.
Telemetria mierząca czasy requestów HTTP, pracę zapytań SQL i parametry puli PostgreSQL pozwala powiązać wolne żądanie z konkretną pracą wykonywaną wewnątrz backendu. Nie zastępuje jednak testów obciążeniowych ani monitoringu produkcyjnego, bo odpowiada na inne pytanie: telemetria mówi, co się dzieje teraz, a testy pokazują, jak system zachowa się przy ruchu, którego jeszcze nie ma. Większa maszyna nie naprawi złej architektury zapytań, tylko przesunie moment, w którym problem wróci.
Co daje Medusa.js, a co zostaje po stronie developera
Medusa.js dostarcza gotowe moduły e-commerce, między innymi Cart, Order, Payment i Inventory, a do tego Workflows, obsługę zdarzeń i moduły infrastrukturalne, w tym blokady. Workflows pozwalają rozbić złożony proces na kroki i zdefiniować funkcje kompensacyjne wykonywane w razie błędu. To mocna podstawa, która nie sprawia jednak, że każde wywołanie zewnętrznego API staje się bezpieczne ani że powstaje jedna transakcja obejmująca PostgreSQL, operatora płatności i pozostałe usługi. Rollback bazy nie cofnie transferu, który operator zdążył wykonać, więc identyfikacja operacji, zapis jej stanu, potwierdzenie skutku, ponowienie i odzyskanie kontroli przy nieznanym wyniku pozostają decyzjami projektowymi.
Z wydajnością jest podobnie. Modularna architektura ułatwia rozwój, ale niestandardowy endpoint nie staje się przez to odporny na obciążenie, a przypadek z kompilacją JIT pokazuje, że źródło spowolnienia bywa poziom niżej niż kod aplikacji.
Z bezpieczeństwem jest podobnie. Medusa.js dostarcza mechanizmy uwierzytelniania i autoryzacji, ale własny endpoint nadal wymaga prawidłowego określenia uprawnień. Sam fakt zalogowania użytkownika nie może wystarczać do odczytania cudzego zamówienia, zmiany jego statusu czy uzyskania dostępu do danych innego klienta. Jeżeli developer dodaje własne API, musi również zadbać o jego zabezpieczenie.
Podobna odpowiedzialność pojawia się przy procesach asynchronicznych. Medusa dostarcza Event Bus i mechanizmy ponawiania, ale to nie oznacza, że każdy nieudany proces biznesowy ma automatycznie własny monitoring, możliwość bezpiecznego replay i ustaloną procedurę odzyskiwania.
Za co więc płaci klient
Wróćmy do dwóch sklepów z początku. W obu da się złożyć zamówienie, oba mogą mieć dobry design, podobną szybkość i te same metody płatności. Różnica siedzi w pracy, której nie widać na zrzucie ekranu.
Ktoś zaprojektował obsługę błędów i stanów pośrednich, zdecydował, które operacje muszą być idempotentne, kiedy potrzebna jest transakcja, a kiedy sama transakcja nic nie załatwi. Ktoś uwzględnił konkurujące żądania, nieaktualne dane, powtórzone webhooki i awarie pomiędzy zapisem w bazie a wywołaniem zewnętrznego API. Ktoś sprawdził, jak system zachowuje się przy równoległym obciążeniu i gdzie dokładnie ucieka czas odpowiedzi.
Do tego dochodzą testy, i to nie tylko te sprawdzające, czy klient może kupić produkt, ale też symulujące timeout po wykonaniu operacji, równoległe refundy czy przerwanie pracy workera. Osobną pracą są testy obciążeniowe i profilowanie zapytań, bez których trudno ocenić, czy aplikacja jest gotowa na wzrost ruchu. Na koniec zostaje utrzymanie: zmiany schematu bazy, wdrożenia, diagnostyka spowolnień, logowanie operacji i możliwość ich wznowienia.
Nie każda aplikacja potrzebuje wszystkich tych mechanizmów. Wiele rozwiązań Artovnii wynika ze specyfiki marketplace'u, szczególnie podziału płatności, rozliczeń sprzedawców i integracji z ich istniejącymi sklepami. Zwykły sklep nie potrzebuje własnego ledgera wypłat ani synchronizacji wielu platform, powinien jednak świadomie określać, co dzieje się przy błędach operacji krytycznych i jakie obciążenie ma obsłużyć. Cena developmentu zależy nie od liczby ekranów, lecz od zakresu odpowiedzialności, jaką system ma przejąć.
Wynajem czy własność, czyli SaaS kontra Medusa.js
Jest jeszcze różnica, której nie widać w wycenie: do kogo należy technologia, na której stoi biznes, i jak dużą masz nad nią kontrolę.
Wynajmując mieszkanie, nie martwisz się o konstrukcję i instalacje. Możesz urządzić je po swojemu, wstawić meble, zmienić kolory ścian, czasem przeprowadzić większy remont, ale zakres zmian zależy od właściciela. Platformy SaaS działają podobnie. Twoja marka, domena, dane i indywidualnie zbudowany storefront pozostają Twoje, natomiast silnik odpowiedzialny za działanie sklepu pozostaje pod kontrolą dostawcy. To on decyduje o kierunku rozwoju oprogramowania, dostępnych możliwościach, zasadach działania API i cenniku.
Otwarty rdzeń Medusa.js jest dostępny na licencji MIT, co daje szerokie możliwości wykorzystania i modyfikacji kodu, przy czym wybrane rozwiązania Enterprise mogą podlegać odrębnym warunkom. Możesz tworzyć własne moduły, przebudowywać logikę zamówień, płatności, stanów magazynowych i integracji, zarządzać wdrożeniami, wybrać infrastrukturę i zdecydować o kierunku rozwoju aplikacji. Prawa do kodu napisanego specjalnie dla Twojego projektu zależą od umowy z wykonawcą, więc warto uregulować je na starcie.
Własny dom kosztuje też po wprowadzeniu. W e-commerce oznacza to hosting, aktualizacje, monitoring, obsługę awarii, bezpieczeństwo infrastruktury i dalszy rozwój oprogramowania. Własny system nie oznacza automatycznie niższego całkowitego kosztu posiadania, daje natomiast możliwość podejmowania decyzji, które w zamkniętym SaaS należą do dostawcy. W SaaS płacisz przede wszystkim za możliwość korzystania z gotowej platformy. W indywidualnym e-commerce na Medusa.js inwestujesz również w rozwój własnej aplikacji i architektury.
Dlatego sklep na Medusa.js może kosztować 5 tys. zł za podstawowe uruchomienie gotowego startera, ale też milion złotych lub więcej za rozbudowane wdrożenie enterprise. Podobną rozpiętość cen wskazuje sam założyciel Medusa.js:
You'll see Medusa shops being sold in all price ranges.
Wskazał również orientacyjne przedziały cenowe, które obserwuje w ofertach agencji i freelancerów:
- 1-10 tys. euro: mniejsze realizacje freelancerskie.
- 10-30 tys. euro: standardowy sklep, realizacja w 1-3 miesiące.
- 30-100 tys. euro: średniej wielkości wdrożenie, realizacja w 2-6 miesięcy.
- 100 tys. do 1 mln euro: wdrożenia enterprise.
To obserwacje rynkowe, nie cennik frameworka, i dotyczą przede wszystkim rynku amerykańskiego oraz europejskiego. Według niego rozwój narzędzi AI, takich jak Claude i Codex, skrócił czas realizacji projektów i przyczynił się do obniżenia wycen.
Wszystkie te projekty mogą korzystać z tego samego frameworka. Różnią się zakresem indywidualizacji, wymaganiami biznesowymi i ilością pracy potrzebnej do wdrożenia oraz późniejszego utrzymania. Koszty i funkcje obu podejść zestawiłem w tekście Medusa.js vs Shopify, uczciwe porównanie dla polskich sklepów w 2026.
Kiedy sklep za 5 tys. zł wystarczy
Jeżeli potrzebujesz standardowego sklepu, gotowy i dojrzały silnik z istniejącymi integracjami bywa rozsądniejszy niż budowanie własnych mechanizmów od zera. Prosty sklep na SaaS może mieć solidną obsługę płatności i zamówień, bo te elementy zostały już opracowane i są utrzymywane przez dostawcę. Dla niewielkiego sklepu, który dopiero zaczyna sprzedaż, to często wystarczy.
Płacisz wtedy za inny model korzystania z technologii i część ograniczeń będzie wynikała z możliwości platformy, nie z umiejętności developera. Jeśli później okaże się, że potrzebujesz niestandardowego procesu zakupowego, rozbudowanego B2B, własnej logiki magazynowej, specyficznego modelu płatności albo nieobsługiwanej integracji, koszt dopasowania biznesu do platformy może przewyższyć początkowe oszczędności. Działa to też w drugą stronę: jeśli wszystkie potrzeby mieszczą się w standardzie SaaS i nie przewidujesz dużych zmian, budowanie własnego systemu bywa nieopłacalne.
Warto przy okazji ustalić, czym ma być MVP. Minimalna wersja produktu nie potrzebuje programu lojalnościowego, zaawansowanych raportów, automatyzacji marketingowych ani dziesięciu integracji, ale od początku powinna poprawnie obsługiwać przyjmowanie pieniędzy, tworzenie zamówień, kontrolę dostępności i dostęp do danych. Nie musi też pierwszego dnia obsługiwać dziesiątek tysięcy równoczesnych użytkowników, powinna jednak mieć określone wymagania wydajnościowe i architekturę, której rozwój nie oznacza przebudowy całego systemu przy pierwszym większym wzroście ruchu. Dobre MVP ogranicza liczbę funkcjonalności, nie jakość operacji krytycznych. Klient powinien przy tym wiedzieć, jakie ograniczenia ma zakupione rozwiązanie i gdzie dzisiejsza oszczędność może oznaczać konieczność poniesienia dodatkowych kosztów w przyszłości.
O co pytać przy wycenie
Przy porównywaniu ofert warto wyjść poza listę funkcjonalności i zapytać o konkretne scenariusze:
- Co stanie się, jeśli operator przyjmie płatność, a sklep nie otrzyma potwierdzenia? Jak system obsłuży dwie równoczesne próby zakupu ostatniego egzemplarza? Czy ponowiony webhook może stworzyć drugie zamówienie?
- Co dzieje się po restarcie backendu w trakcie operacji, czy przerwane zadania da się wznowić i jak system reaguje na chwilową niedostępność bazy albo zewnętrznej usługi? Co stanie się z zadaniem, które nie powiedzie się nawet po wszystkich zaplanowanych ponowieniach, i w jaki sposób administrator dowie się o problemie?
- Dla jakiego ruchu projektowany jest system, czy przewidziano testy obciążeniowe, jak będą diagnozowane wolne endpointy i zapytania, czy ktoś patrzy na p95 i p99, a nie tylko na średnią?
- Czy otrzymasz dostęp do repozytorium, kto będzie miał prawa do indywidualnie stworzonego kodu, czy możesz zmienić wykonawcę i przenieść system na inną infrastrukturę?
Przy SaaS warto wiedzieć, które elementy sklepu należą do Ciebie, jakie dane możesz wyeksportować i co się stanie, jeżeli dostawca zmieni warunki usługi.
Nie każda odpowiedź musi oznaczać pisanie własnego modułu, bo część problemów bywa już poprawnie obsłużona przez platformę, gotowy komponent albo dostawcę usług. Liczy się to, czy wykonawca rozumie problem, zna odpowiedzialność poszczególnych elementów systemu i potrafi uzasadnić proponowane rozwiązanie. Wycena oparta na liście ekranów nie pokazuje całego zakresu pracy, a oferta obiecująca pełne bezpieczeństwo, wydajność i niezawodność bez określenia granic tych gwarancji powinna budzić ostrożność.
Co z tego zostaje
Sklep, który poprawnie przeprowadza klienta przez zakup, niekoniecznie jest przygotowany na awarie, które prędzej czy później wystąpią. Blokada przy finalizacji koszyka, klucz idempotencji przy płatności, reconciliation refundu, obsługa nieudanych zadań asynchronicznych i diagnostyka zapytań PostgreSQL to praca, której użytkownik nigdy nie zobaczy, a która decyduje o tym, co się stanie w gorszym dniu.
Medusa.js daje przy tym dużą kontrolę nad własnym systemem i swobodę budowania niestandardowych rozwiązań. Razem z nimi przychodzi odpowiedzialność za zaprojektowanie, przetestowanie i utrzymanie tej logiki, bo framework dostarcza narzędzia, a nie gotowe decyzje architektoniczne.
Przy planowaniu customowego sklepu warto więc pytać nie tylko o to, co będzie działać, ale też co się stanie, kiedy coś przestanie działać, i jak system zachowa się przy ruchu, którego dzisiaj jeszcze nie ma. Jeżeli planujesz taki projekt, chętnie przejdę przez te scenariusze na konkretach Twojego biznesu. Tym właśnie zajmuję się w AppCrates, budując sklepy i platformy e-commerce na Medusa.js.


