{"id":32256,"date":"2025-10-20T05:00:00","date_gmt":"2025-10-20T03:00:00","guid":{"rendered":"https:\/\/sii.pl\/blog\/?p=32256"},"modified":"2025-10-20T10:20:35","modified_gmt":"2025-10-20T08:20:35","slug":"spring-x-mongodb-jak-zabezpieczyc-swoje-transakcje","status":"publish","type":"post","link":"https:\/\/sii.pl\/blog\/spring-x-mongodb-jak-zabezpieczyc-swoje-transakcje\/","title":{"rendered":"Spring x MongoDB \u2013 jak zabezpieczy\u0107 swoje transakcje"},"content":{"rendered":"\n<p>Niedawno zesp\u00f3\u0142 Coma wr\u00f3ci\u0142 po latach, by zagra\u0107 kilka koncert\u00f3w. Wskrzeszona popularno\u015b\u0107 muzyk\u00f3w rozgrzewa fan\u00f3w do czerwono\u015bci. W ko\u0144cu wybija godzina zero. Tysi\u0105ce fan\u00f3w od\u015bwie\u017ca stron\u0119. Bilety znikaj\u0105 b\u0142yskawicznie \u2013 jak tostery na Black Friday. Zosta\u0142 jeden, kt\u00f3ry widz\u0105 Adam i Ewa. <strong>Oboje szybko klikaj\u0105 \u201eRezerwuj\u0119 i p\u0142ac\u0119\u201d.<\/strong> Klik! Strona prze\u0142adowuje si\u0119, pojawia si\u0119 k\u00f3\u0142ko \u0142adowania&#8230; a w tle, w serwerowni, rozgrywa si\u0119 dramat, od kt\u00f3rego zale\u017cy, czy to Adam, czy Ewa lub kt\u00f3rekolwiek z nich b\u0119dzie mog\u0142o \u015bpiewa\u0107 \u201eLeszka \u017bukowskiego\u201d pod scen\u0105\u2026 I czy w og\u00f3le uda si\u0119 im o tym dowiedzie\u0107.<\/p>\n\n\n\n<p><strong>W tym u\u0142amku sekundy system musi odj\u0105\u0107 bilet ze stanu, utworzy\u0107 rezerwacj\u0119 i przetworzy\u0107 p\u0142atno\u015b\u0107.<\/strong> A co, je\u015bli oba \u017c\u0105dania zd\u0105\u017c\u0105 zobaczy\u0107, \u017ce ostatni bilet jest dost\u0119pny? Dwie osoby maj\u0105 by\u0107 obci\u0105\u017cone p\u0142atno\u015bci\u0105? Albo je\u015bli zewn\u0119trzny operator p\u0142atno\u015bci odpowie z b\u0142\u0119dem lub po prostu up\u0142ynie jego czas na odpowied\u017a?<\/p>\n\n\n\n<p>Je\u015bli bilet zosta\u0142y ju\u017c zarezerwowany, ale p\u0142atno\u015b\u0107 si\u0119 nie powiod\u0142a, ani Adam ani Ewa nie dostan\u0105 biletu, ale nikt inny te\u017c ich nie kupi. Zostaj\u0105 zamro\u017cone w cyfrowej pr\u00f3\u017cni. To scenariusz, kt\u00f3ry sp\u0119dza sen z powiek ka\u017cdemu \u2013 nie tylko fanom Comy, ale te\u017c nam \u2013 deweloperom.<\/p>\n\n\n\n<p><strong>Jak wi\u0119c zaprojektowa\u0107 system, kt\u00f3ry z gracj\u0105 obs\u0142uguje takie sytuacje? Odpowiedzi\u0105 jest po\u0142\u0105czenie gwarancji transakcyjnych nowoczesnych baz danych z wzorcami projektowymi stworzonymi dla \u015bwiata system\u00f3w rozproszonych<\/strong>.<\/p>\n\n\n\n<p>W tym artykule, analizuj\u0105c kod demonstracyjnego projektu, zobaczymy, jak to zrobi\u0107 dobrze.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Po lekturze tego artyku\u0142u b\u0119dziesz wiedzie\u0107<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Jakie s\u0105 fundamentalne mechanizmy transakcyjne w MongoDB.<\/li>\n\n\n\n<li>Jak radzi\u0107 sobie z konfliktami wsp\u00f3\u0142bie\u017cno\u015bci, u\u017cywaj\u0105c izolacji migawkowej.<\/li>\n\n\n\n<li>Jak \u0142\u0105czy\u0107 silne gwarancje atomowo\u015bci (ACID) ze wzorcami zapewniaj\u0105cymi ostateczn\u0105 sp\u00f3jno\u015b\u0107 w jednym procesie biznesowym.<\/li>\n\n\n\n<li>Czym jest wzorzec Transactional Outbox i jak rozwi\u0105zuje problem zawodnej komunikacji w systemach rozproszonych.<\/li>\n\n\n\n<li>Jak zarz\u0105dza\u0107 wieloetapowymi procesami za pomoc\u0105 wzorca Saga w modelu choreografii, aby zarz\u0105dza\u0107 d\u0142ugotrwa\u0142ymi procesami biznesowymi.<\/li>\n\n\n\n<li>Jak zapewni\u0107 poprawne i wydajne przetwarzanie zada\u0144 w tle w \u015brodowisku wieloinstancyjnym.<\/li>\n\n\n\n<li>Czym jest <strong>ostateczna sp\u00f3jno\u015b\u0107<\/strong> i dlaczego jest to \u015bwiadomy, architektoniczny kompromis.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Architektura<\/strong><\/h2>\n\n\n\n<p>Zacznijmy od om\u00f3wienia z lotu high levelowego architektury, kt\u00f3ra b\u0119dzie realizowana w ramach tej demonstracji. <a href=\"https:\/\/github.com\/BartDro\/MongoTransactions\" target=\"_blank\" rel=\"noopener\" title=\"\" rel=\"nofollow\" >Ca\u0142y kod jest dost\u0119pny do pobrania i w\u0142asnor\u0119cznego uruchomienia<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><img decoding=\"async\" width=\"1024\" height=\"369\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/image1-1024x369.png\" alt=\"Architektura\" class=\"wp-image-32257\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/image1-1024x369.png 1024w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/image1-300x108.png 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/image1-768x277.png 768w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/image1-1536x553.png 1536w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/image1.png 1588w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Ryc. 1 Architektura<\/figcaption><\/figure>\n\n\n\n<p>Na pierwszy rzut oka widzimy trzy g\u0142\u00f3wne akty tego procesu:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Atomowe z\u0142o\u017cenie zam\u00f3wienia:<\/strong> Wszystko zaczyna si\u0119 od akcji klienta, kt\u00f3ra inicjuje jedn\u0105, niepodzieln\u0105 transakcj\u0119. W ramach tej operacji system jednocze\u015bnie rezerwuje zasoby (np. bilety), tworzy dokument rezerwacji i \u2013 co kluczowe \u2013 zapisuje zadanie p\u0142atno\u015bci w bazie danych. Ten ostatni krok to serce wzorca Outbox, kt\u00f3ry zostanie om\u00f3wiony szerzej w dalszej cz\u0119\u015bci.<\/li>\n\n\n\n<li><strong>Asynchroniczne przetwarzanie p\u0142atno\u015bci:<\/strong> W tle dzia\u0142a niezale\u017cny komponent, Payment Scheduler. Dzia\u0142a on jak listonosz, kt\u00f3ry regularnie sprawdza baz\u0119 danych w poszukiwaniu zaplanowanych zada\u0144. Gdy znajdzie takie zadanie, podejmuje pr\u00f3b\u0119 wykonania p\u0142atno\u015bci, komunikuj\u0105c si\u0119 z zewn\u0119trznym systemem. Po otrzymaniu odpowiedzi publikuje zdarzenie adekwatne do rezultatu: PaymentSucceededEvent lub PaymentFailedEvent.<\/li>\n\n\n\n<li><strong>Finalizacja lub Kompensacja<\/strong>: System w spos\u00f3b reaktywny nas\u0142uchuje na opublikowane zdarzenia. Sukces p\u0142atno\u015bci uruchamia transakcj\u0119 potwierdzaj\u0105c\u0105, kt\u00f3ra finalizuje rezerwacj\u0119. Pora\u017cka z kolei inicjuje transakcj\u0119 kompensuj\u0105c\u0105, kt\u00f3ra \u201esprz\u0105ta\u201d po nieudanej operacji \u2013 na przyk\u0142ad anuluje rezerwacj\u0119 i zwalnia zablokowane wcze\u015bniej bilety, przywracaj\u0105c system do sp\u00f3jnego stanu \u2013 to miejsce, w kt\u00f3rym mruga do nas <strong>filozofia Wzorca Sagi<\/strong>.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Implementacja<\/strong><\/h2>\n\n\n\n<p>Wszystko zaczyna si\u0119 od prostego \u017c\u0105dania HTTP, kt\u00f3re trafia do naszego kontrolera. Widzimy tu standardowy endpoint, kt\u00f3ry przyjmuje \u017c\u0105danie i deleguje ca\u0142\u0105 prac\u0119 do serwisu. <strong>Zwr\u00f3\u0107 uwag\u0119 na status HttpStatus.ACCEPTED<\/strong>. To subtelna wskaz\u00f3wka dla klienta, \u017ce jego pro\u015bba zosta\u0142a przyj\u0119ta do przetworzenia, ale proces jeszcze si\u0119 nie zako\u0144czy\u0142.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Zarezerwuj wszystko albo nic<\/strong><\/h2>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n@RestController\n@RequestMapping(&quot;\/reservations&quot;)\nclass ReservationController(private val reservationService: ReservationService) {\n    \/\/ ...\n    @PostMapping\n    fun createReservation(@RequestBody request: ReservationRequest): ResponseEntity&amp;lt;ReservationResponse&gt; {\n        logger.info(&quot;Received reservation request: $request&quot;)\n        return ResponseEntity.status(HttpStatus.ACCEPTED) \/\/ Przyj\u0119to do realizacji\n            .body(reservationService.createReservation(request).toReservationResponse())\n    }\n}\n<\/pre><\/div>\n\n\n<p>St\u0105d przechodzimy do serwisu, kt\u00f3ry zaczyna sekwencj\u0119 atomowych krok\u00f3w:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclass ReservationService(...) {\n\n   @Transactional\n   @Retryable(value = &#x5B;DataIntegrityViolationException::class], maxAttempts = 3, backoff = Backoff(delay = 2000, multiplier = 2.0))\n    fun createReservation(reservationRequest: ReservationRequest){\n\n        val requestedEvent = eventRepository.findById(reservationRequest.eventId)\n            .orElseThrow { EventNotFound() }\n            .toModel()\n\n        if (requestedEvent.areTicketsInsufficient(reservationRequest.ticketCount)) {\n            logger.error(&quot;Not enough tickets available for event: ${requestedEvent.eventId}&quot;)\n            throw NotEnoughTicketException()\n        }\n\n        val reservedTickets = requestedEvent.reserveTickets(reservationRequest.ticketCount)\n        eventRepository.save(requestedEvent.toDocument())\n        logger.info(&quot;Reserved tickets: $reservedTickets for event: ${requestedEvent.eventId}&quot;)\n\n        val reservation = reservationRepository.save(\n            ReservationDocument(\n                eventId = reservationRequest.eventId,\n                userId = reservationRequest.userId,\n                ticketCount = reservationRequest.ticketCount,\n                reservedAt = Instant.now(clock),\n                reservationStatus = Reservation.ReservationStatus.PENDING.name\n            )\n        ).toModel()\n\n        logger.info(&quot;Created reservation: $reservation for user: ${reservationRequest.userId}&quot;)\n\n        paymentOutbox.save(\n            PaymentOutboxTask(\n                paymentStatus = PaymentStatus.SCHEDULED.name,\n                paymentRequest = reservation.toPaymentRequest(reservationRequest, reservedTickets),\n                createdAt = Instant.now(clock),\n            )\n        )\n        logger.info(&quot;Payment outbox task scheduled for reservation: ${reservation.reservationId}&quot;)\n    }\n<\/pre><\/div>\n\n\n<p><strong>Adnotacja @Transactional<\/strong> ze Spring Data MongoDB to nasza <strong>obietnica dla systemu<\/strong>: wszystkie operacje zapisu do bazy danych wewn\u0105trz tej metody (eventRepository.save, reservationRepository.save, paymentOutbox.save) musz\u0105 powie\u015b\u0107 si\u0119 jako jedna, niepodzielna ca\u0142o\u015b\u0107. Je\u015bli na kt\u00f3rymkolwiek etapie wyst\u0105pi b\u0142\u0105d (np. konflikt zapisu, bo kto\u015b inny kupi\u0142 te bilety w tym samym momencie), ca\u0142a transakcja zostanie wycofana. Stan bazy danych wr\u00f3ci do punktu wyj\u015bcia \u2013 tak, jakby \u017c\u0105danie Adama lub Ewy nigdy nie nadesz\u0142o. <strong>To jest nasza atomowo\u015b\u0107 w pierwszej ods\u0142onie.<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Adnotacja @Retryable<\/strong><\/h3>\n\n\n\n<p>Kluczowa tak\u017ce jest <strong>adnotacja @Retryable<\/strong>, kt\u00f3ra instruuje Springa, by w przypadku specyficznych b\u0142\u0119d\u00f3w (np. jednoczesnej pr\u00f3by zapisu przez Adama i Ew\u0119) automatycznie ponowi\u0142 ca\u0142\u0105 transakcj\u0119. To prosta, ale pot\u0119\u017cna technika zwi\u0119kszaj\u0105ca odporno\u015b\u0107 systemu na chwilowe konflikty. Aby w pe\u0142ni zrozumie\u0107 dzia\u0142aj\u0105cy tu mechanizm, musimy zajrze\u0107 pod mask\u0119 transakcji w MongoDB. Dzia\u0142aj\u0105 one w oparciu o tzw. izolacj\u0119 migawkow\u0105 (ang. snapshot isolation).<\/p>\n\n\n\n<p>Zobaczmy, co dzieje si\u0119, gdy Adam i Ewa klikaj\u0105 przycisk w tej samej milisekundzie, walcz\u0105c o ostatni bilet.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Dwie r\u00f3wnoleg\u0142e rzeczywisto\u015bci:<\/strong> System rozpoczyna dwie osobne transakcje, a ka\u017cda z nich tworzy w\u0142asn\u0105 \u201emigawk\u0119\u201d (snapshot) stanu bazy danych. W obu tych \u201emigawkach\u201d widnieje jeden dost\u0119pny bilet. Co wa\u017cne, jedna transakcja nie blokuje odczytu drugiej. Obie, dzia\u0142aj\u0105c na swoich kopiach rzeczywisto\u015bci, dochodz\u0105 do tego samego wniosku: \u201eEkstra, ostatni wolny bilet \u2013 rezerwuj\u0119!\u201d.<\/li>\n\n\n\n<li><strong>Kto pierwszy, ten lepszy<\/strong>: Za\u0142\u00f3\u017cmy, \u017ce transakcja Adama jest o u\u0142amek sekundy szybsza i jako pierwsza wykonuje operacj\u0119 zapisu (eventRepository.save). W tym momencie, mimo \u017ce transakcja nie jest jeszcze zatwierdzona, MongoDB zak\u0142ada na modyfikowanym dokumencie tymczasow\u0105 blokad\u0119 zapisu. Gdy transakcja Ewy, dzia\u0142aj\u0105ca na swojej (ju\u017c nieaktualnej) migawce, r\u00f3wnie\u017c pr\u00f3buje wykona\u0107 zapis na tym samym dokumencie, natychmiast napotyka t\u0119 blokad\u0119.<\/li>\n\n\n\n<li><strong>Fail Fast:<\/strong> Transakcja Ewy nie czeka do ko\u0144ca. Od razu wie, \u017ce inna operacja ju\u017c \u201ezaklepa\u0142a\u201d ten dokument, wi\u0119c natychmiast przerywa transakcj\u0119 Ewy, rzucaj\u0105c b\u0142\u0105d konfliktu zapisu (WriteConflictException), kt\u00f3ry w \u015bwiecie Springa jest cz\u0119sto mapowany na DataIntegrityViolationException.<\/li>\n\n\n\n<li><strong>Automatyczne samoleczenie:<\/strong> I w\u0142a\u015bnie w tym momencie do gry wkracza @Retryable. Zamiast zwr\u00f3ci\u0107 Ewie niejasny b\u0142\u0105d techniczny, mechanizm ten \u201e\u0142apie\u201d wyj\u0105tek konfliktu i m\u00f3wi: \u201e<em>Spokojnie, to cz\u0119sty problem przy du\u017cym ruchu. Spr\u00f3bujmy jeszcze raz!<\/em>\u201d. Ca\u0142a metoda createReservation dla Ewy jest uruchamiana ponownie.<\/li>\n\n\n\n<li><strong>Nowa, aktualna rzeczywisto\u015b\u0107:<\/strong> Podczas drugiej pr\u00f3by transakcja Ewy tworzy now\u0105 migawk\u0119 danych. Tym razem w bazie nie ma ju\u017c dost\u0119pnych bilet\u00f3w \u2013 zosta\u0142y poprawnie zarezerwowane przez Adama. Logika wewn\u0105trz metody (if (areTicketsInsufficient&#8230;)) natychmiast to wykrywa i transakcja ko\u0144czy si\u0119 w spos\u00f3b kontrolowany, z jasnym komunikatem biznesowym: \u201eNiestety, bilet\u00f3w ju\u017c nie ma\u201d.<\/li>\n<\/ol>\n\n\n\n<p>Dzi\u0119ki @Retryable system samoleczy si\u0119 z przej\u015bciowego problemu technicznego i przekszta\u0142ca go w jednoznaczn\u0105 i prawdziw\u0105 odpowied\u017a biznesow\u0105: \u201ePrzepraszamy, kto\u015b by\u0142 szybszy\u201d.<\/p>\n\n\n\n<p>To mechanizm, kt\u00f3ry:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Zapobiega nadsprzeda\u017cy i chaosowi w danych.<\/li>\n\n\n\n<li>Gwarantuje sp\u00f3jno\u015b\u0107 biznesow\u0105 nawet pod du\u017cym obci\u0105\u017ceniem.<\/li>\n\n\n\n<li>Zapewnia znacznie lepsze do\u015bwiadczenie u\u017cytkownika, buduj\u0105c jego zaufanie do platformy.<\/li>\n<\/ul>\n\n\n\n<p>W skr\u00f3cie to most, kt\u00f3ry \u0142\u0105czy techniczn\u0105 strategi\u0119 obs\u0142ugi konflikt\u00f3w wsp\u00f3\u0142bie\u017cno\u015bci z p\u0142ynnym i logicznym do\u015bwiadczeniem dla klienta ko\u0144cowego.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Niezawodny listonosz \u2013 wzorzec Outbox<\/strong><\/h2>\n\n\n\n<p>Zauwa\u017c, \u017ce w powy\u017cszej transakcji nie ma bezpo\u015bredniego wywo\u0142ania API p\u0142atno\u015bci. To celowy zabieg. Wywo\u0142anie zewn\u0119trznego serwisu w trakcie trwania transakcji bazodanowej to <strong>proszenie si\u0119 o k\u0142opoty<\/strong>. Taki serwis mo\u017ce odpowiada\u0107 wolno, blokuj\u0105c cenne zasoby bazy danych, albo mo\u017ce by\u0107 niedost\u0119pny, co spowodowa\u0142oby wycofanie ca\u0142ej naszej transakcji, mimo \u017ce rezerwacja bilet\u00f3w by\u0142a mo\u017cliwa.<\/p>\n\n\n\n<p>Zamiast tego stosujemy wzorzec Outbox. W ramach samej transakcji tworzymy wszystkie potrzebne rezerwacje i zapisy \u2013 a na ko\u0144cu zapisujemy zadanie, kt\u00f3re dopiero ma si\u0119 wykona\u0107 po scomitowaniu transakcji. Dzi\u0119ki temu unikamy opisanych wy\u017cej problem\u00f3w oraz mamy pewno\u015b\u0107, \u017ce klient zostanie obci\u0105\u017cone p\u0142atno\u015bci\u0105 \u2013 <strong>wy\u0142\u0105cznie<\/strong> \u2013 w sytuacji, gdy uda si\u0119 atomowo zarezerwowa\u0107 dla niego wszystkie niezb\u0119dne zasoby.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npaymentOutbox.save(\n    PaymentOutboxTask(\n        paymentStatus = PaymentStatus.SCHEDULED.name,\n        paymentRequest = reservation.toPaymentRequest(reservationRequest, reservedTickets),\n        createdAt = Instant.now(clock),\n    )\n)\n<\/pre><\/div>\n\n\n<p>Poniewa\u017c ten zapis jest cz\u0119\u015bci\u0105 transakcji atomowej, <strong>mamy \u017celazn\u0105 gwarancj\u0119: je\u015bli rezerwacja zosta\u0142a utworzona, to zadanie p\u0142atno\u015bci r\u00f3wnie\u017c istnieje<\/strong>. Zrzucili\u015bmy z siebie ci\u0119\u017car natychmiastowego przetworzenia p\u0142atno\u015bci, zamieniaj\u0105c go na gwarantowane zadanie \u201edo wykonania w przysz\u0142o\u015bci\u201d. <\/p>\n\n\n\n<p><a href=\"https:\/\/microservices.io\/patterns\/data\/transactional-outbox.html\" target=\"_blank\" rel=\"noopener\" title=\"\" rel=\"nofollow\" >Dla szerszego kontekstu zapraszam do dokumentu<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Przetwarzanie asynchroniczne oraz choreografia sagi<\/strong><\/h2>\n\n\n\n<p>Teraz czas na PaymentOutboxScheduler. To komponent, kt\u00f3ry dzia\u0142a w tle i w regularnych odst\u0119pach czasu sprawdza nasz\u0105 \u201eskrzynk\u0119 nadawcz\u0105\u201d.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n@Component\nclass PaymentOutboxScheduler(\n    private val paymentOutboxRepository: PaymentOutboxRepository,\n    private val paymentService: PaymentService\n) {\n\n    private val logger = logger {}\n\n    @Scheduled(fixedDelayString = &quot;PT5S&quot;)\n    fun processPaymentOutbox() {\n        val startedTask = paymentOutboxRepository.findAndStartPaymentTask() ?: return\n        logger.info(&quot;Started payment outbox task: $startedTask for processing&quot;)\n        val processedTask = paymentService.process(startedTask)\n        paymentOutboxRepository.save(processedTask)\n    }\n\n    @Scheduled(fixedDelayString = &quot;PT90S&quot;)\n    @SchedulerLock(name = &quot;outboxPaymentTaskCleanupLock&quot;, lockAtMostFor = &quot;5m&quot;, lockAtLeastFor = &quot;30s&quot;)\n    fun cleanupStuckTasks() {\n        paymentOutboxRepository.resetStuckTasks()\n        paymentOutboxRepository.findDeadTasks().forEach { paymentService.notifyPaymentResult(it) }\n    }\n}\n<\/pre><\/div>\n\n\n<p>Adnotacja @Scheduled sprawia, \u017ce Spring regularnie wykonuje metod\u0119 processPaymentOutbox, kt\u00f3rej zadaniem jest:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>znalezienie gotowego do wykonania zadania,<\/li>\n\n\n\n<li>podj\u0119cie zadania,<\/li>\n\n\n\n<li>faktyczna pr\u00f3ba obci\u0105\u017cenia klienta p\u0142atno\u015bci\u0105 w paymentService.process(startedTask),<\/li>\n\n\n\n<li>wyemitowanie do systemu odpowiedniego eventu,<\/li>\n\n\n\n<li>nast\u0119pnie zapisanie zadania ze statusem zako\u0144czone.<\/li>\n<\/ul>\n\n\n\n<p>Warto po\u015bwi\u0119ci\u0107 chwil\u0119 na om\u00f3wienie samej metody znajduj\u0105cej zadania. Pod spodem na poziomie repozytorium u\u017cywa ona metody findAndModify. Zosta\u0142a ona zaprojektowana jako jedna atomowa akcja na znalezionym dokumencie \u2013 dlatego w\u0142a\u015bnie zabezpiecza przed sytuacj\u0105 w kt\u00f3rej wi\u0119cej ni\u017c jedna instancja mog\u0142aby spr\u00f3bowa\u0107 podj\u0105\u0107 to samo zadanie z bazy.<\/p>\n\n\n\n<p><strong><em>Nota architektoniczna:<\/em><\/strong> Wzorzec \u201epollera\u201d bazodanowego, kt\u00f3ry cyklicznie skanuje kolekcj\u0119, jest eleganckim i prostym rozwi\u0105zaniem. Warto jednak pami\u0119ta\u0107, \u017ce w systemach o ekstremalnie wysokiej przepustowo\u015bci (tysi\u0105ce zada\u0144 na sekund\u0119) takie skanowanie mo\u017ce sta\u0107 si\u0119 w\u0105skim gard\u0142em. W takich scenariuszach <strong>rozwa\u017ca si\u0119 u\u017cycie dedykowanych, wyspecjalizowanych system\u00f3w kolejkowych <\/strong>(np. Kafka, RabbitMQ). Dla wielu przypadk\u00f3w (szczeg\u00f3lnie kodu demonstruj\u0105cego) pokazane rozwi\u0105zanie stanowi <strong>idealny kompromis pomi\u0119dzy prostot\u0105 implementacji a wydajno\u015bci\u0105.<\/strong><\/p>\n\n\n\n<p>Dla kontrastu, inne podej\u015bcie zosta\u0142o zastosowane w drugiej metodzie \u2013 cleanupStuckTasks.<\/p>\n\n\n\n<p>Jest to pewnego rodzaju nadzorca, kt\u00f3rego zadaniem jest cykliczne uruchamianie si\u0119 w du\u017co rzadszym interwale oraz znajdowanie zada\u0144, kt\u00f3re z jakiego\u015b powodu utkn\u0119\u0142y w stanie PROCESSING. Zostaj\u0105 one wtedy zresetowane do stanu SCHEDULED, przez co znowu mog\u0105 by\u0107 podj\u0119te. Jest to kluczowy mechanizm samonaprawczy, ale wymaga jednej fundamentalnej gwarancji od zewn\u0119trznego systemu p\u0142atno\u015bci \u2013 <strong>idempotentno\u015bci<\/strong>.<\/p>\n\n\n\n<p>Co, je\u015bli zadanie faktycznie zosta\u0142o wykonane, p\u0142atno\u015b\u0107 pobrana, ale nasz serwis uleg\u0142 awarii, zanim zd\u0105\u017cy\u0142 zaktualizowa\u0107 status w outboxie? Ponowne przetworzenie zresetowanego zadania nie mo\u017ce prowadzi\u0107 do podw\u00f3jnego obci\u0105\u017cenia klienta. Dlatego nasze \u017c\u0105danie p\u0142atno\u015bci musi zawiera\u0107 unikalny klucz idempotencji (idempotencyKey), kt\u00f3ry gwarantuje, \u017ce operator p\u0142atno\u015bci przetworzy dan\u0105 transakcj\u0119 tylko raz, niezale\u017cnie od liczby pr\u00f3b. Dodatkowo w momencie ponownego ich wykonania zostanie zwi\u0119kszona wersja zadania.<\/p>\n\n\n\n<p>Ten fakt jest kluczowy dla drugiej metody \u201cfindDeadTasks\u201d, kt\u00f3rej zadaniem jest zlokalizowanie tych zada\u0144, kt\u00f3rych wersja jest wi\u0119ksza ni\u017c 3 oraz nadal s\u0105 w stanie \u201ePROCESSING\u201d. Oznacza to, \u017ce z jakiego\u015b powodu nie mog\u0105 by\u0107 zrealizowane, przez co trzeba je finalnie uzna\u0107 za nieudane oraz zwolni\u0107 powi\u0105zane z nimi zasoby poprzez wyemitowanie zdarzenia o b\u0142\u0119dzie p\u0142atno\u015bci.<\/p>\n\n\n\n<p>R\u00f3\u017cnica mi\u0119dzy tymi cyklicznymi procesami nie polega jednak tylko na r\u00f3\u017cnych interwa\u0142ach, ale na demonstracji innego podej\u015bcia, jak mo\u017cna te\u017c obs\u0142ugiwa\u0107 cykliczne zadania w \u015brodowisku wielu instancji. Drugi proces przeszukuje ca\u0142e kolekcje i operuje na listach znalezionych dokument\u00f3w.<\/p>\n\n\n\n<p><strong>Co si\u0119 stanie, gdy uruchomi si\u0119 w tym samym czasie na wielu instancjach<\/strong>? Mongo nie zapewnia atomowej metody findManyAndUpdate, wi\u0119c trzeba najpierw znale\u017a\u0107 wszystkie spe\u0142niaj\u0105ce kryteria dokumenty, a nast\u0119pnie je zmodyfikowa\u0107. Nie chcemy sytuacji, w kt\u00f3rej ka\u017cda instancja przeszukuje te same kolekcje. Dlatego potrzebny jest mechanizm, kt\u00f3ry sprawi, \u017ce w tylko jedna instancja mo\u017ce podj\u0105\u0107 takie zaplanowane zadanie, a pozosta\u0142e powinny w tym czasie je ignorowa\u0107.<\/p>\n\n\n\n<p>To <strong>cz\u0119ste wyzwanie w \u015brodowisku wielu instancji,<\/strong> dlatego te\u017c powsta\u0142y gotowe rozwi\u0105zania jak ShedLock, kt\u00f3re rozwi\u0105zuj\u0105 ten problem. Przy minimalnej konfiguracji zostanie nam udost\u0119pniona adnotacja @SchedulerLock, kt\u00f3ra, jak nazwa wskazuje, dzia\u0142a dok\u0142adnie jak mechanizm Lock\u00f3w, kt\u00f3ry mo\u017cemy zna\u0107 z zarz\u0105dzania wielow\u0105tkowo\u015bci\u0105 albo mechanizm\u00f3w blokowania z baz danych. Zak\u0142ada ona po prostu locka w specjalnej, nowej kolekcji bazy danych dla tej konkretnej instancji. Dzi\u0119ki temu inne instancje, chc\u0105c podj\u0105\u0107 to samo zadanie, nie b\u0119d\u0105 mog\u0142y tego zrobi\u0107, je\u015bli b\u0119dzie za\u0142o\u017cony ju\u017c lock.<\/p>\n\n\n\n<p><strong>Daje nam to pewno\u015b\u0107<\/strong>, \u017ce raz wczytane dane w procesie cyklicznym b\u0119d\u0105 wczytane tylko przez jedn\u0105 instancj\u0119 na cykl oraz dodatkowo zyskujemy oszcz\u0119dno\u015b\u0107 zasob\u00f3w wiedz\u0105c, \u017ce baza danych nie procesuje niepotrzebnie wiele razy tych samym zapyta\u0144.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Werdykt \u2013 transakcja potwierdzaj\u0105ca lub kompensuj\u0105ca<\/strong><\/h2>\n\n\n\n<p>Serwis ReservationService nas\u0142uchuje na dwa typy zdarze\u0144, kt\u00f3re s\u0105 rezultatem pr\u00f3by p\u0142atno\u015bci. Ka\u017cde z nich inicjuje osobn\u0105, atomow\u0105 transakcj\u0119, kt\u00f3ra stanowi ostatni akt w naszym procesie biznesowym.<\/p>\n\n\n\n<p><strong>Scenariusz pozytywny \u2013 p\u0142atno\u015b\u0107 udana:<\/strong><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n@EventListener\n@Transactional\nfun handleSuccessfulPaymentEvent(event: SuccessPaymentEvent) {\n    reservationRepository.confirm(event.reservationId)\n    eventRepository.confirm(event.eventId, event.ticketIds)\n    logger.info(&quot;Payment successful...&quot;)\n}\n<\/pre><\/div>\n\n\n<p>Gdy p\u0142atno\u015b\u0107 si\u0119 powiedzie, uruchamiana jest nowa transakcja, kt\u00f3ra finalizuje proces. Zmienia status rezerwacji z PENDING na CONFIRMED, a bilety na SOLD. Od tego momentu bilety oficjalnie nale\u017c\u0105 do Adama lub Ewy.<\/p>\n\n\n\n<p><strong>Scenariusz negatywny \u2013 p\u0142atno\u015b\u0107 nieudana:<\/strong><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n@EventListener\n@Transactional\nfun handleFailedPaymentEvent(event: FailedPaymentEvent) {\n    reservationRepository.cancel(event.reservationId)\n    eventRepository.release(event.eventId, event.ticketIds)\n    logger.info(&quot;Payment failed... tickets released&quot;)\n}\n<\/pre><\/div>\n\n\n<p>Je\u015bli p\u0142atno\u015b\u0107 si\u0119 nie powiedzie, uruchamiana jest <strong>transakcja kompensuj\u0105ca<\/strong>. To kluczowy element, kt\u00f3ry przywraca porz\u0105dek w systemie. Rezerwacja jest anulowana, a co najwa\u017cniejsze, bilety s\u0105 zwalniane (eventRepository.release) i wracaj\u0105 do og\u00f3lnodost\u0119pnej puli, gotowe do zakupu przez kogo\u015b innego.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Wzorzec Saga<\/strong><\/h3>\n\n\n\n<p>Dlaczego ten wieloetapowy proces nazywamy Sag\u0105 (mimo, \u017ce bardzo prost\u0105) na potrzeby prezentacji. <strong>Wzorzec Saga<\/strong> to mechanizm zarz\u0105dzania sp\u00f3jno\u015bci\u0105 danych w systemach rozproszonych bez potrzeby stosowania d\u0142ugotrwa\u0142ych, blokuj\u0105cych transakcji rozproszonych, kt\u00f3re s\u0105 cz\u0119sto niepraktyczne i s\u0142abo si\u0119 skaluj\u0105.<\/p>\n\n\n\n<p>Proces rezerwacji idealnie wpisuje si\u0119 w definicj\u0119 Sagi, poniewa\u017c:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Sk\u0142ada si\u0119 z sekwencji lokalnych transakcji<\/strong>, kt\u00f3re razem tworz\u0105 kompletny proces biznesowy:\n<ul class=\"wp-block-list\">\n<li>Transakcja rezerwuje bilet i tworzy zadanie w outboxie.<\/li>\n\n\n\n<li>Schedulowana operacja przetwarza p\u0142atno\u015b\u0107.<\/li>\n\n\n\n<li>Transakcja potwierdza rezerwacj\u0119 lub j\u0105 anuluje.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Ma transakcje kompensuj\u0105ce:<\/strong> Je\u015bli kt\u00f3ry\u015b z krok\u00f3w po pierwszej transakcji zawiedzie (np. p\u0142atno\u015b\u0107 si\u0119 nie powiedzie), Saga uruchamia akcje kompensuj\u0105ce (handleFailedPaymentEvent), kt\u00f3re cofaj\u0105 skutki wcze\u015bniejszych krok\u00f3w. W naszym przypadku jest to zwolnienie bilet\u00f3w i anulowanie rezerwacji.<\/li>\n\n\n\n<li><strong>Zapewnia sp\u00f3jno\u015b\u0107 na poziomie biznesowym:<\/strong> Chocia\u017c system nie jest sp\u00f3jny w ka\u017cdej milisekundzie (istnieje stan PENDING), Saga gwarantuje, \u017ce ca\u0142y proces biznesowy zako\u0144czy si\u0119 w jednym z dw\u00f3ch sp\u00f3jnych stan\u00f3w: albo rezerwacja jest w pe\u0142ni op\u0142acona i potwierdzona, albo jest anulowana, a bilety wracaj\u0105 do puli. Nie ma mo\u017cliwo\u015bci, by system utkn\u0105\u0142 w stanie po\u015brednim.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Koordynacja Sagi<\/strong><\/h3>\n\n\n\n<p>Warto w tym miejscu wspomnie\u0107 o dw\u00f3ch g\u0142\u00f3wnych sposobach koordynacji Sagi:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Choreografia:<\/strong> Model zastosowany w tym scenariuszu. Poszczeg\u00f3lne serwisy (lub komponenty) subskrybuj\u0105 zdarzenia emitowane przez inne i reaguj\u0105 na nie, wykonuj\u0105c swoj\u0105 cz\u0119\u015b\u0107 pracy. Nie ma centralnego zarz\u0105dcy. Komunikacja jest zdecentralizowana, co \u015bwietnie sprawdza si\u0119 w prostszych przep\u0142ywach.<\/li>\n\n\n\n<li><strong>Orkiestracja:<\/strong> W tym modelu istnieje centralny komponent (orkiestrator), kt\u00f3ry dyryguje ca\u0142ym procesem, m\u00f3wi\u0105c poszczeg\u00f3lnym serwisom, jakie kroki maj\u0105 wykona\u0107. Jest to rozwi\u0105zanie lepsze dla bardziej z\u0142o\u017conych sag z wieloma krokami i skomplikowan\u0105 logik\u0105 warunkow\u0105.<\/li>\n<\/ul>\n\n\n\n<p><strong>Dla przypadku rezerwacji bilet\u00f3w model choreografii jest idealny ze wzgl\u0119du na swoj\u0105 prostot\u0119 i odporno\u015b\u0107.<\/strong> Warto jednak pami\u0119ta\u0107, \u017ce w miar\u0119 wzrostu liczby krok\u00f3w w procesie biznesowym, model orkiestracji cz\u0119sto staje si\u0119 \u0142atwiejszy w zarz\u0105dzaniu, monitorowaniu i debugowaniu.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Deal z czasem, czyli czym jest \u201eostateczna sp\u00f3jno\u015b\u0107\u201d<\/strong><\/h2>\n\n\n\n<p>Warto na chwil\u0119 si\u0119 zatrzyma\u0107 i zrozumie\u0107 istotny kompromis, kt\u00f3ry zosta\u0142 wybrany. Od momentu, gdy u\u017cytkownik klikn\u0105\u0142 \u201eKupuj\u0119\u201d, do chwili, gdy jego rezerwacja zosta\u0142a ostatecznie potwierdzona lub anulowana, min\u0119\u0142o troch\u0119 czasu. W tym okresie system by\u0142 w stanie przej\u015bciowym: bilety by\u0142y zarezerwowane, ale rezerwacja mia\u0142a status PENDING.<\/p>\n\n\n\n<p>To jest w\u0142a\u015bnie ostateczna sp\u00f3jno\u015b\u0107 (ang. eventual consistency). Gwarantujemy, \u017ce system w ko\u0144cu dojdzie do stanu sp\u00f3jnego, ale niekoniecznie stanie si\u0119 to natychmiast. Dla biznesu oznacza to, \u017ce u\u017cytkownik na ekranie m\u00f3g\u0142 zobaczy\u0107 komunikat \u201ePrzetwarzamy Twoj\u0105 rezerwacj\u0119, prosimy czeka\u0107&#8230;\u201d, zamiast natychmiastowego potwierdzenia. To \u015bwiadoma decyzja architektoniczna. <strong>W zamian za ten kr\u00f3tki okres niepewno\u015bci zyskujemy system, kt\u00f3ry jest niepor\u00f3wnywalnie bardziej odporny na awarie i lepiej si\u0119 skaluje<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>OUTRO<\/strong><\/h2>\n\n\n\n<p>Prze\u015bledzili\u015bmy ca\u0142\u0105 podr\u00f3\u017c \u017c\u0105dania \u2013 od jednego klikni\u0119cia, przez sie\u0107 transakcji i zdarze\u0144, a\u017c do ostatecznego, sp\u00f3jnego stanu. Zamiast budowa\u0107 monolityczny, kruchy proces, stworzyli\u015bmy odporny i elastyczny przep\u0142yw pracy.<\/p>\n\n\n\n<p>Po\u0142\u0105czyli\u015bmy twarde gwarancje atomowo\u015bci transakcji MongoDB, aby zapewni\u0107 integralno\u015b\u0107 danych w krytycznym momencie rezerwacji, z asynchroniczn\u0105 niezawodno\u015bci\u0105 wzorc\u00f3w Outbox i Saga. To podej\u015bcie pozwala nam budowa\u0107 z\u0142o\u017cone procesy biznesowe, wiedz\u0105c, \u017ce chwilowa niedost\u0119pno\u015b\u0107 zewn\u0119trznego serwisu czy awaria naszej w\u0142asnej aplikacji nie zrujnuje sp\u00f3jno\u015bci naszych danych. <strong>To w\u0142a\u015bnie tak wygl\u0105daj\u0105 fundamenty nowoczesnych, solidnych system\u00f3w backendowych.<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/oferty-pracy\/\" target=\"_blank\" rel=\"noreferrer noopener\"><img decoding=\"async\" width=\"737\" height=\"170\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/praca-PL-k-1.jpg\" alt=\"oferty pracy\" class=\"wp-image-32259\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/praca-PL-k-1.jpg 737w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/praca-PL-k-1-300x69.jpg 300w\" sizes=\"(max-width: 737px) 100vw, 737px\" \/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Scena po napisach<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Bonus 1: Dlaczego transakcje wymagaj\u0105 Replica Set?<\/strong><\/h3>\n\n\n\n<p>Odpowied\u017a jest prosta i elegancka: transakcje zosta\u0142y zbudowane na fundamencie mechanizmu, kt\u00f3ry w MongoDB istnia\u0142 od lat \u2013 replikacji. Sercem ka\u017cdego Replica Set jest oplog (operations log), czyli chronologiczny dziennik wszystkich operacji zapisu. W\u0119z\u0142y wt\u00f3rne (ang. secondary) \u201e\u015bledz\u0105\u201d ten dziennik, aby powiela\u0107 zmiany i utrzymywa\u0107 sp\u00f3jno\u015b\u0107 z w\u0119z\u0142em g\u0142\u00f3wnym (ang. primary).<\/p>\n\n\n\n<p>In\u017cynierowie MongoDB, projektuj\u0105c transakcje, wykorzystali ten istniej\u0105cy, przetestowany w boju i niezwykle odporny mechanizm. Ka\u017cdy krok transakcji, jej post\u0119p i ostateczna decyzja (commit lub rollback) s\u0105 zapisywane w\u0142a\u015bnie w oplogu. To on staje si\u0119 jedynym, autorytatywnym \u017ar\u00f3d\u0142em prawdy o stanie transakcji.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Bonus 2: Jak transakcja ACID mo\u017ce by\u0107 rozproszona?<\/strong><\/h3>\n\n\n\n<p>Kluczem jest zrozumienie, \u017ce atomowo\u015b\u0107 transakcji jest gwarantowana i koordynowana w jednym miejscu: na w\u0119\u017ale primary. \u201eRozproszenie\u201d w tym kontek\u015bcie nie oznacza negocjowania wyniku transakcji mi\u0119dzy w\u0119z\u0142ami. Oznacza propagacj\u0119 sp\u00f3jnego, atomowo zatwierdzonego wyniku w celu zapewnienia wysokiej dost\u0119pno\u015bci i trwa\u0142o\u015bci danych.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Bonus 3: Pu\u0142apka readPreference i nadrz\u0119dno\u015b\u0107 w\u0119z\u0142a primary<\/strong><\/h3>\n\n\n\n<p>Aby zoptymalizowa\u0107 wydajno\u015b\u0107 i zmniejszy\u0107 op\u00f3\u017anienia, w MongoDB cz\u0119sto u\u017cywa si\u0119 ustawienia readPreference. Jest to instrukcja dla sterownika bazy danych, kt\u00f3ra m\u00f3wi mu, z kt\u00f3rego w\u0119z\u0142a w klastrze ma czyta\u0107 dane. Popularnym wyborem jest \u201enearest\u201d, co oznacza odpytywanie geograficznie najbli\u017cszego serwera, niezale\u017cnie od tego, czy jest to primary, czy secondary.<\/p>\n\n\n\n<p><strong>Tu jednak czai si\u0119 pu\u0142apka<\/strong>. Za\u0142\u00f3\u017cmy, \u017ce globalnie ustawili\u015bmy readPreference na \u201enearest\u201d. Dlaczego wi\u0119c operacje w naszej transakcji nagle przestaj\u0105 dzia\u0142a\u0107?<\/p>\n\n\n\n<p>Odpowied\u017a zn\u00f3w le\u017cy w sp\u00f3jno\u015bci. Transakcja, aby spe\u0142ni\u0107 obietnic\u0119 ACID, musi operowa\u0107 na idealnie sp\u00f3jnej i aktualnej migawce danych. W\u0119z\u0142y secondary z natury replikuj\u0105 dane z niewielkim op\u00f3\u017anieniem. Gdyby transakcja mog\u0142a odczyta\u0107 dane z w\u0119z\u0142a secondary, ryzykowa\u0142aby operowanie na nieaktualnych danych, co mog\u0142oby prowadzi\u0107 do kosztownych b\u0142\u0119d\u00f3w.<\/p>\n\n\n\n<p>Dlatego silnik MongoDB narzuca \u017celazn\u0105 zasad\u0119: wszystkie operacje wewn\u0105trz aktywnej transakcji, zar\u00f3wno odczyty, jak i zapisy, musz\u0105 by\u0107 kierowane do w\u0119z\u0142a primary. Globalne ustawienie readPreference jest na czas trwania transakcji ignorowane na rzecz absolutnej sp\u00f3jno\u015bci. <a href=\"https:\/\/github.com\/BartDro\/MongoTransactions\" target=\"_blank\" rel=\"noopener\" title=\"\" rel=\"nofollow\" >Spos\u00f3b konfiguracji jest pokazany w za\u0142\u0105czonym repozytorium<\/a>.<\/p>\n\n\n<div class=\"kk-star-ratings kksr-auto kksr-align-left kksr-valign-bottom\"\n    data-payload='{&quot;align&quot;:&quot;left&quot;,&quot;id&quot;:&quot;32256&quot;,&quot;slug&quot;:&quot;default&quot;,&quot;valign&quot;:&quot;bottom&quot;,&quot;ignore&quot;:&quot;&quot;,&quot;reference&quot;:&quot;auto&quot;,&quot;class&quot;:&quot;&quot;,&quot;count&quot;:&quot;2&quot;,&quot;legendonly&quot;:&quot;&quot;,&quot;readonly&quot;:&quot;&quot;,&quot;score&quot;:&quot;5&quot;,&quot;starsonly&quot;:&quot;&quot;,&quot;best&quot;:&quot;5&quot;,&quot;gap&quot;:&quot;11&quot;,&quot;greet&quot;:&quot;&quot;,&quot;legend&quot;:&quot;5\\\/5 ( votes: 2)&quot;,&quot;size&quot;:&quot;18&quot;,&quot;title&quot;:&quot;Spring x MongoDB \u2013 jak zabezpieczy\u0107 swoje transakcje&quot;,&quot;width&quot;:&quot;139.5&quot;,&quot;_legend&quot;:&quot;{score}\\\/{best} ( {votes}: {count})&quot;,&quot;font_factor&quot;:&quot;1.25&quot;}'>\n            \n<div class=\"kksr-stars\">\n    \n<div class=\"kksr-stars-inactive\">\n            <div class=\"kksr-star\" data-star=\"1\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"2\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"3\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"4\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"5\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n    \n<div class=\"kksr-stars-active\" style=\"width: 139.5px;\">\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n<\/div>\n                \n\n<div class=\"kksr-legend\" style=\"font-size: 14.4px;\">\n            5\/5 ( votes: 2)    <\/div>\n    <\/div>\n","protected":false},"excerpt":{"rendered":"<p>Niedawno zesp\u00f3\u0142 Coma wr\u00f3ci\u0142 po latach, by zagra\u0107 kilka koncert\u00f3w. Wskrzeszona popularno\u015b\u0107 muzyk\u00f3w rozgrzewa fan\u00f3w do czerwono\u015bci. W ko\u0144cu wybija &hellip; <a class=\"continued-btn\" href=\"https:\/\/sii.pl\/blog\/spring-x-mongodb-jak-zabezpieczyc-swoje-transakcje\/\">Continued<\/a><\/p>\n","protected":false},"author":601,"featured_media":32261,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_editorskit_title_hidden":false,"_editorskit_reading_time":0,"_editorskit_is_block_options_detached":false,"_editorskit_block_options_position":"{}","inline_featured_image":false,"footnotes":""},"categories":[1314],"tags":[1546,1512,930],"class_list":["post-32256","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development-na-twardo","tag-przeglad-narzedzi","tag-poradnik","tag-e-commerce"],"acf":[],"aioseo_notices":[],"republish_history":[],"featured_media_url":"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2025\/10\/Computer-4.jpg","category_names":["Development na twardo"],"_links":{"self":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/32256"}],"collection":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/users\/601"}],"replies":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/comments?post=32256"}],"version-history":[{"count":1,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/32256\/revisions"}],"predecessor-version":[{"id":32263,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/32256\/revisions\/32263"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/media\/32261"}],"wp:attachment":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/media?parent=32256"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/categories?post=32256"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/tags?post=32256"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}