From 3e1e516a49ebba2c25c09555e6fb827b064bd01f Mon Sep 17 00:00:00 2001 From: Tanya Johari Date: Sun, 5 Oct 2025 15:44:18 +0530 Subject: [PATCH 1/5] feat: Implement Transactional Outbox pattern Resolves #2671 --- microservices-transactional-outbox/pom.xml | 100 ++++++++++++++ .../com/iluwatar/transactionaloutbox/App.java | 57 ++++++++ .../transactionaloutbox/Customer.java | 43 ++++++ .../transactionaloutbox/CustomerService.java | 62 +++++++++ .../transactionaloutbox/EventPoller.java | 122 ++++++++++++++++++ .../transactionaloutbox/MessageBroker.java | 44 +++++++ .../transactionaloutbox/OutboxEvent.java | 74 +++++++++++ .../transactionaloutbox/OutboxRepository.java | 59 +++++++++ .../main/resources/META-INF/persistence.xml | 22 ++++ pom.xml | 1 + 10 files changed, 584 insertions(+) create mode 100644 microservices-transactional-outbox/pom.xml create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/App.java create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/Customer.java create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/CustomerService.java create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/EventPoller.java create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/MessageBroker.java create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxEvent.java create mode 100644 microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxRepository.java create mode 100644 microservices-transactional-outbox/src/main/resources/META-INF/persistence.xml diff --git a/microservices-transactional-outbox/pom.xml b/microservices-transactional-outbox/pom.xml new file mode 100644 index 000000000000..16cc2f7adab5 --- /dev/null +++ b/microservices-transactional-outbox/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + microservices-transactional-outbox + + + + org.slf4j + slf4j-api + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + + + ch.qos.logback + logback-classic + + + org.hibernate.orm + hibernate-core + 6.4.4.Final + + + com.h2database + h2 + 2.2.224 + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-junit-jupiter + 5.16.1 + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.transactionaloutbox.App + + + + + + + + + diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/App.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/App.java new file mode 100644 index 000000000000..20c3018d44e3 --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/App.java @@ -0,0 +1,57 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.transactionaloutbox; + +import jakarta.persistence.Persistence; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class App { + + public static void main(String[] args) throws Exception { + var entityManagerFactory = Persistence.createEntityManagerFactory("transactional-outbox-pu"); + var entityManager = entityManagerFactory.createEntityManager(); + + var customerService = new CustomerService(entityManager); + var messageBroker = new MessageBroker(); + var eventPoller = new EventPoller(entityManager, messageBroker); + + eventPoller.start(); + + LOGGER.info("Running simulation..."); + Thread.sleep(1000); + + customerService.createCustomer("john.doe"); + Thread.sleep(5000); + + customerService.createCustomer("jane.doe"); + Thread.sleep(5000); + + eventPoller.stop(); + entityManager.close(); + entityManagerFactory.close(); + LOGGER.info("Simulation finished."); + } +} diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/Customer.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/Customer.java new file mode 100644 index 000000000000..8be0cf70e904 --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/Customer.java @@ -0,0 +1,43 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.transactionaloutbox; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Entity +public class Customer { + + private final String username; + @Setter @Id @GeneratedValue private Integer id; + + public Customer(String username) { + this.username = username; + } +} diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/CustomerService.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/CustomerService.java new file mode 100644 index 000000000000..76ad46fd13c3 --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/CustomerService.java @@ -0,0 +1,62 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.transactionaloutbox; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CustomerService { + + private final EntityManager entityManager; + private final OutboxRepository outboxRepository; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public CustomerService(EntityManager entityManager) { + this.entityManager = entityManager; + this.outboxRepository = new OutboxRepository(entityManager); + } + + public void createCustomer(String username) throws Exception { + entityManager.getTransaction().begin(); + try { + var customer = new Customer(username); + entityManager.persist(customer); + + String payload = objectMapper.writeValueAsString(customer); + var event = new OutboxEvent("CUSTOMER_CREATED", payload); + outboxRepository.save(event); + + entityManager.getTransaction().commit(); + LOGGER.info("SUCCESS: Customer and OutboxEvent saved transactionally."); + + } catch (Exception e) { + entityManager.getTransaction().rollback(); + LOGGER.error("ERROR: Transaction rolled back."); + throw e; + } + } +} diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/EventPoller.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/EventPoller.java new file mode 100644 index 000000000000..5464ef7862af --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/EventPoller.java @@ -0,0 +1,122 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.transactionaloutbox; + +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EventPoller { + + private static final Logger LOGGER = LoggerFactory.getLogger(EventPoller.class); + private static final int MAX_RETRIES = 3; + private static final long RETRY_DELAY_MS = 1000; + private static final java.security.SecureRandom RANDOM = new java.security.SecureRandom(); + + @Getter + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private final EntityManager entityManager; + private final OutboxRepository outboxRepository; + private final MessageBroker messageBroker; + @Getter private long processedEventsCount = 0; + @Getter private long failedEventsCount = 0; + + public EventPoller(EntityManager entityManager, MessageBroker messageBroker) { + this.entityManager = entityManager; + this.outboxRepository = new OutboxRepository(entityManager); + this.messageBroker = messageBroker; + } + + public void start() { + scheduler.scheduleAtFixedRate(this::processOutboxEvents, 0, 2, TimeUnit.SECONDS); + LOGGER.info("EventPoller started."); + } + + public void stop() { + scheduler.shutdown(); + LOGGER.info( + "EventPoller stopped with {} events processed and {} failures.", + processedEventsCount, + failedEventsCount); + } + + void processOutboxEvents() { + LOGGER.info("Polling for unprocessed events..."); + entityManager.getTransaction().begin(); + try { + List events = outboxRepository.findUnprocessedEvents(); + if (events.isEmpty()) { + LOGGER.info("No new events found."); + } else { + LOGGER.info("Found {} events to process.", events.size()); + for (var event : events) { + processEventWithRetry(event); + } + } + entityManager.getTransaction().commit(); + } catch (Exception e) { + LOGGER.error("Error processing outbox events, rolling back transaction.", e); + entityManager.getTransaction().rollback(); + failedEventsCount++; + } + } + + private void processEventWithRetry(OutboxEvent event) { + int retryCount = 0; + while (retryCount < MAX_RETRIES) { + try { + messageBroker.sendMessage(event); + outboxRepository.markAsProcessed(event); + processedEventsCount++; + LOGGER.info("Successfully processed event."); + return; + } catch (Exception e) { + retryCount++; + if (retryCount < MAX_RETRIES) { + LOGGER.warn( + "Failed to process event (attempt {}/{}). Retrying...", retryCount, MAX_RETRIES); + try { + long sleepTime = + Math.min(RETRY_DELAY_MS * (1L << (retryCount - 1)) + RANDOM.nextLong(100), 10000L); + Thread.sleep(sleepTime); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } else { + LOGGER.error("Failed to process event after {} attempts", MAX_RETRIES); + failedEventsCount++; + } + } + } + } +} diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/MessageBroker.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/MessageBroker.java new file mode 100644 index 000000000000..5f3c12c0f30f --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/MessageBroker.java @@ -0,0 +1,44 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.transactionaloutbox; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MessageBroker { + + public void sendMessage(OutboxEvent event) { + LOGGER.info( + "MESSAGE_BROKER: Sending message... Event ID: {}, Payload: {}", + event.getId(), + event.getPayload()); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + LOGGER.info("MESSAGE_BROKER: Message sent successfully."); + } +} diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxEvent.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxEvent.java new file mode 100644 index 000000000000..5288e4bc01ce --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxEvent.java @@ -0,0 +1,74 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.transactionaloutbox; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "OUTBOX") +public class OutboxEvent { + + private final LocalDateTime createdAt; + @Setter @Getter @Id @GeneratedValue private Integer id; + @Getter private String eventType; + @Setter @Getter private String payload; + @Setter @Getter private boolean processed; + @Setter @Getter private Long sequenceNumber; + + public OutboxEvent() { + this.createdAt = LocalDateTime.now(); + this.processed = false; + } + + public OutboxEvent(String eventType, String payload) { + this(); + this.eventType = eventType; + this.payload = payload; + } + + @Override + public String toString() { + return "OutboxEvent{" + + "id=" + + id + + ", eventType='" + + eventType + + '\'' + + ", payload='" + + payload + + '\'' + + ", processed=" + + processed + + ", createdAt=" + + createdAt + + '}'; + } +} diff --git a/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxRepository.java b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxRepository.java new file mode 100644 index 000000000000..be440d4e0583 --- /dev/null +++ b/microservices-transactional-outbox/src/main/java/com/iluwatar/transactionaloutbox/OutboxRepository.java @@ -0,0 +1,59 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.transactionaloutbox; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.LockModeType; +import java.util.List; + +public class OutboxRepository { + + private final EntityManager entityManager; + + public OutboxRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public void save(OutboxEvent event) { + entityManager.persist(event); + } + + public void markAsProcessed(OutboxEvent event) { + event.setProcessed(true); + entityManager.merge(event); + } + + private static final int BATCH_SIZE = 100; + + public List findUnprocessedEvents() { + return entityManager + .createQuery( + "SELECT e FROM OutboxEvent e WHERE e.processed = false ORDER BY e.sequenceNumber", + OutboxEvent.class) + .setMaxResults(BATCH_SIZE) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .getResultList(); + } +} diff --git a/microservices-transactional-outbox/src/main/resources/META-INF/persistence.xml b/microservices-transactional-outbox/src/main/resources/META-INF/persistence.xml new file mode 100644 index 000000000000..c7314863e1d2 --- /dev/null +++ b/microservices-transactional-outbox/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,22 @@ + + + + org.hibernate.jpa.HibernatePersistenceProvider + com.iluwatar.transactionaloutbox.Customer + com.iluwatar.transactionaloutbox.OutboxEvent + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8337c97966da..f288c41f2e15 100644 --- a/pom.xml +++ b/pom.xml @@ -167,6 +167,7 @@ microservices-distributed-tracing microservices-idempotent-consumer microservices-log-aggregation + microservices-transactional-outbox model-view-controller model-view-intent model-view-presenter From 199067fb514a5a90e53d0cbeb3ad581aa6d4fbe9 Mon Sep 17 00:00:00 2001 From: Tanya Johari Date: Sun, 5 Oct 2025 15:44:54 +0530 Subject: [PATCH 2/5] feat: Add test cases Resolves #2671 --- .../iluwatar/transactionaloutbox/AppTest.java | 36 ++++ .../CustomerServiceTests.java | 58 ++++++ .../transactionaloutbox/EventPollerTests.java | 169 ++++++++++++++++++ .../MessageBrokerTests.java | 27 +++ .../transactionaloutbox/OutboxEventTests.java | 73 ++++++++ .../OutboxRepositoryTests.java | 36 ++++ .../test/resources/META-INF/persistence.xml | 24 +++ 7 files changed, 423 insertions(+) create mode 100644 microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java create mode 100644 microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java create mode 100644 microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java create mode 100644 microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java create mode 100644 microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java create mode 100644 microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java create mode 100644 microservices-transactional-outbox/src/test/resources/META-INF/persistence.xml diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java new file mode 100644 index 000000000000..a0f695b75214 --- /dev/null +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java @@ -0,0 +1,36 @@ +package com.iluwatar.transactionaloutbox; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.mockito.Mockito.mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; + +@ExtendWith(MockitoExtension.class) +class AppTest { + + @Test + void testMainExecution() { + assertDoesNotThrow(() -> App.main(new String[] {})); + } + + @Test + void testSimulateCustomerCreation() { + var entityManagerFactory = mock(EntityManagerFactory.class); + var entityManager = mock(EntityManager.class); + var customerService = new CustomerService(entityManager); + var messageBroker = new MessageBroker(); + var eventPoller = new EventPoller(entityManager, messageBroker); + + assertNotNull(entityManagerFactory); + assertNotNull(entityManager); + assertNotNull(customerService); + assertNotNull(messageBroker); + assertNotNull(eventPoller); + } +} diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java new file mode 100644 index 000000000000..9b1741ffeb71 --- /dev/null +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java @@ -0,0 +1,58 @@ +package com.iluwatar.transactionaloutbox; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Tests for {@link CustomerService}. */ +@ExtendWith(MockitoExtension.class) +class CustomerServiceTests { + + @Mock private EntityManager entityManager; + + @Mock private EntityTransaction transaction; + + private CustomerService customerService; + + @BeforeEach + void setup() { + when(entityManager.getTransaction()).thenReturn(transaction); + customerService = new CustomerService(entityManager); + } + + @Test + void shouldCreateCustomerAndOutboxEventInSameTransaction() throws Exception { + var username = "testUser"; + + customerService.createCustomer(username); + + verify(transaction).begin(); + verify(entityManager, times(2)).persist(any()); + verify(transaction).commit(); + verify(transaction, never()).rollback(); + } + + @Test + void shouldRollbackTransactionOnFailure() { + + var username = "testUser"; + doThrow(new RuntimeException("Test failure")).when(entityManager).persist(any(Customer.class)); + + assertThrows(Exception.class, () -> customerService.createCustomer(username)); + verify(transaction).begin(); + verify(transaction).rollback(); + verify(transaction, never()).commit(); + } +} diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java new file mode 100644 index 000000000000..f18bfdbd9c5f --- /dev/null +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java @@ -0,0 +1,169 @@ +package com.iluwatar.transactionaloutbox; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.LockModeType; +import jakarta.persistence.TypedQuery; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Tests for {@link EventPoller}. */ +@ExtendWith(MockitoExtension.class) +class EventPollerTests { + + private static final String TEST_EVENT = "TEST_EVENT"; + private static final String TEST_PAYLOAD = "payload"; + + @Mock private EntityManager entityManager; + @Mock private EntityTransaction transaction; + @Mock private MessageBroker messageBroker; + @Mock private TypedQuery query; + private EventPoller eventPoller; + + @BeforeEach + void setup() { + when(entityManager.getTransaction()).thenReturn(transaction); + when(entityManager.createQuery(anyString(), eq(OutboxEvent.class))).thenReturn(query); + when(query.setMaxResults(any(Integer.class))).thenReturn(query); + when(query.setLockMode(any(LockModeType.class))).thenReturn(query); + eventPoller = new EventPoller(entityManager, messageBroker); + } + + @Test + void shouldProcessEventsSuccessfully() { + var event = new OutboxEvent("EVENT_1", "payload1"); + when(query.getResultList()).thenReturn(Collections.singletonList(event)); + + eventPoller.processOutboxEvents(); + + verify(messageBroker).sendMessage(event); + verify(transaction).begin(); + verify(transaction).commit(); + assertEquals(1, eventPoller.getProcessedEventsCount()); + } + + @Test + void shouldHandleFailureAndRetry() { + var event = new OutboxEvent(TEST_EVENT, TEST_PAYLOAD); + when(query.getResultList()).thenReturn(Collections.singletonList(event)); + doThrow(new RuntimeException("First attempt")) + .doNothing() + .when(messageBroker) + .sendMessage(any(OutboxEvent.class)); + + eventPoller.processOutboxEvents(); + + verify(messageBroker, times(2)).sendMessage(event); + assertEquals(1, eventPoller.getProcessedEventsCount()); + assertEquals(0, eventPoller.getFailedEventsCount()); + } + + @Test + void shouldHandleInterruptedThread() { + var event = new OutboxEvent(TEST_EVENT, TEST_PAYLOAD); + when(query.getResultList()).thenReturn(Collections.singletonList(event)); + doThrow(new RuntimeException("Processing fails")) + .when(messageBroker) + .sendMessage(any(OutboxEvent.class)); + Thread.currentThread().interrupt(); + + eventPoller.processOutboxEvents(); + + verify(transaction).begin(); + verify(transaction).commit(); + verify(messageBroker).sendMessage(event); + assertEquals(0, eventPoller.getProcessedEventsCount()); + assertEquals( + 0, eventPoller.getFailedEventsCount(), "Interrupted events are not counted as failures"); + assertTrue(Thread.interrupted(), "Interrupt flag should be preserved"); + } + + @Test + void shouldHandleEmptyEventList() { + when(query.getResultList()).thenReturn(Collections.emptyList()); + + eventPoller.processOutboxEvents(); + + verify(transaction).begin(); + verify(transaction).commit(); + assertEquals(0, eventPoller.getProcessedEventsCount()); + assertEquals(0, eventPoller.getFailedEventsCount()); + } + + @Test + void shouldHandleMaxRetryAttempts() { + var event = new OutboxEvent(TEST_EVENT, TEST_PAYLOAD); + when(query.getResultList()).thenReturn(Collections.singletonList(event)); + doThrow(new RuntimeException("Failed processing")) + .when(messageBroker) + .sendMessage(any(OutboxEvent.class)); + + eventPoller.processOutboxEvents(); + + verify(messageBroker, times(3)).sendMessage(event); + assertEquals(0, eventPoller.getProcessedEventsCount()); + assertEquals(1, eventPoller.getFailedEventsCount()); + } + + @Test + void shouldProcessMultipleEvents() { + var event1 = new OutboxEvent("EVENT_1", "payload1"); + var event2 = new OutboxEvent("EVENT_2", "payload2"); + when(query.getResultList()).thenReturn(java.util.Arrays.asList(event1, event2)); + + eventPoller.processOutboxEvents(); + + verify(messageBroker).sendMessage(event1); + verify(messageBroker).sendMessage(event2); + verify(transaction).begin(); + verify(transaction).commit(); + assertEquals(2, eventPoller.getProcessedEventsCount()); + assertEquals(0, eventPoller.getFailedEventsCount()); + } + + @Test + void shouldRollbackTransactionWhenRepositoryFails() { + var dbException = new RuntimeException("Database connection failed"); + when(query.getResultList()).thenThrow(dbException); + + eventPoller.processOutboxEvents(); + + verify(transaction).begin(); + verify(transaction).rollback(); + verify(transaction, never()).commit(); + + assertEquals(1, eventPoller.getFailedEventsCount()); + assertEquals(0, eventPoller.getProcessedEventsCount()); + } + + @Test + void shouldStartAndStopCorrectly() throws Exception { + eventPoller.start(); + + assertFalse(eventPoller.getScheduler().isShutdown()); + assertFalse(eventPoller.getScheduler().isTerminated()); + + eventPoller.stop(); + + assertTrue(eventPoller.getScheduler().isShutdown()); + assertTrue(eventPoller.getScheduler().awaitTermination(1, TimeUnit.SECONDS)); + assertTrue(eventPoller.getScheduler().isTerminated()); + } +} diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java new file mode 100644 index 000000000000..8074e329c395 --- /dev/null +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java @@ -0,0 +1,27 @@ +package com.iluwatar.transactionaloutbox; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MessageBrokerTests { + private final MessageBroker messageBroker = new MessageBroker(); + + @Test + void shouldSendMessageSuccessfully() { + var event = new OutboxEvent("TEST_EVENT", "test_payload"); + messageBroker.sendMessage(event); + assertFalse(Thread.interrupted(), "Thread should not be interrupted"); + } + + @Test + void shouldHandleInterruptedException() { + var event = new OutboxEvent("TEST_EVENT", "test_payload"); + Thread.currentThread().interrupt(); + + messageBroker.sendMessage(event); + + assertTrue(Thread.interrupted(), "Thread interrupt flag should be preserved"); + } +} diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java new file mode 100644 index 000000000000..17d26a29d44f --- /dev/null +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java @@ -0,0 +1,73 @@ +package com.iluwatar.transactionaloutbox; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** Tests for {@link OutboxEvent}. */ +class OutboxEventTests { + + @Test + void newOutboxEventShouldBeUnprocessed() { + var eventType = "CUSTOMER_CREATED"; + var payload = "{\"customerId\":1}"; + + var event = new OutboxEvent(eventType, payload); + + assertNotNull(event); + assertEquals(eventType, event.getEventType()); + assertEquals(payload, event.getPayload()); + assertFalse(event.isProcessed()); + assertNotNull(event.toString(), "toString should include createdAt value"); + } + + @Test + void processedEventShouldBeMarkedAsProcessed() { + var event = new OutboxEvent("TEST_EVENT", "payload"); + event.setId(1); + event.setProcessed(true); + + assertTrue(event.isProcessed()); + assertEquals(Integer.valueOf(1), event.getId()); + } + + @Test + void eventsShouldMaintainSequentialOrder() { + var event1 = new OutboxEvent("EVENT_1", "payload1"); + var event2 = new OutboxEvent("EVENT_2", "payload2"); + + event1.setSequenceNumber(1L); + event2.setSequenceNumber(2L); + + assertEquals(Long.valueOf(1L), event1.getSequenceNumber()); + assertEquals(Long.valueOf(2L), event2.getSequenceNumber()); + assertTrue(event1.getSequenceNumber() < event2.getSequenceNumber()); + } + + @Test + void shouldFormatToStringWithAllFields() { + var event = new OutboxEvent("TEST_EVENT", "payload"); + event.setId(123); + event.setSequenceNumber(456L); + event.setProcessed(true); + + var toString = event.toString(); + + assertTrue(toString.contains("id=123"), "toString should contain id"); + assertTrue(toString.contains("eventType='TEST_EVENT'"), "toString should contain eventType"); + assertTrue(toString.contains("payload='payload'"), "toString should contain payload"); + assertTrue(toString.contains("processed=true"), "toString should contain processed status"); + assertTrue(toString.contains("createdAt="), "toString should contain createdAt timestamp"); + } + + @Test + void defaultConstructorShouldInitializeBasicFields() { + var event = new OutboxEvent(); + + assertNotNull(event.toString(), "toString should include createdAt value"); + assertFalse(event.isProcessed(), "Should not be processed by default"); + } +} diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java new file mode 100644 index 000000000000..9202ac0e6799 --- /dev/null +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java @@ -0,0 +1,36 @@ +package com.iluwatar.transactionaloutbox; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Tests for {@link OutboxRepository}. */ +@ExtendWith(MockitoExtension.class) +class OutboxRepositoryTests { + + @Mock private EntityManager entityManager; + private OutboxRepository repository; + + @BeforeEach + void setup() { + repository = new OutboxRepository(entityManager); + } + + @Test + void shouldSaveAndMarkEventAsProcessed() { + var event = new OutboxEvent("TEST_EVENT", "payload"); + + repository.save(event); + repository.markAsProcessed(event); + + verify(entityManager).persist(event); + verify(entityManager).merge(event); + assertTrue(event.isProcessed()); + } +} diff --git a/microservices-transactional-outbox/src/test/resources/META-INF/persistence.xml b/microservices-transactional-outbox/src/test/resources/META-INF/persistence.xml new file mode 100644 index 000000000000..5d3a4619055a --- /dev/null +++ b/microservices-transactional-outbox/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,24 @@ + + + + + org.hibernate.jpa.HibernatePersistenceProvider + com.iluwatar.transactionaloutbox.OutboxEvent + com.iluwatar.transactionaloutbox.Customer + + + + + + + + + + + + + \ No newline at end of file From fddc220e5c3d9025a5f6b130f290edb5ba0ce620 Mon Sep 17 00:00:00 2001 From: Tanya Johari Date: Sun, 5 Oct 2025 15:45:01 +0530 Subject: [PATCH 3/5] feat: Add docs Resolves #2671 --- microservices-transactional-outbox/README.md | 184 ++++++++++++++++++ ...ervices-transactional-outbox-flowchart.png | Bin 0 -> 33520 bytes .../microservices-transactional-outbox.puml | 69 +++++++ 3 files changed, 253 insertions(+) create mode 100644 microservices-transactional-outbox/README.md create mode 100644 microservices-transactional-outbox/etc/microservices-transactional-outbox-flowchart.png create mode 100644 microservices-transactional-outbox/etc/microservices-transactional-outbox.puml diff --git a/microservices-transactional-outbox/README.md b/microservices-transactional-outbox/README.md new file mode 100644 index 000000000000..05201761a1a0 --- /dev/null +++ b/microservices-transactional-outbox/README.md @@ -0,0 +1,184 @@ +--- +title: "Microservices Transactional Outbox Pattern in Java: Ensuring Reliable Messaging" +shortTitle: Transactional Outbox +description: "Learn how the Transactional Outbox pattern guarantees reliable message delivery between microservices by leveraging a local database transaction, achieving eventual consistency." +category: Integration +language: en +tag: + - Microservices + - Messaging + - Fault tolerance + - Decoupling + - Data consistency + - Enterprise patterns +--- + +## Also known as + +* Outbox Pattern +* Reliable Messaging Pattern + +## Intent of Microservices Transactional Outbox Design Pattern + +To ensure that messages are reliably sent from a microservice as part of a single, atomic database transaction, preventing data loss and inconsistencies in distributed systems. + +## Detailed Explanation of Microservices Transactional Outbox Pattern with Real-World Examples + +Real-world example +> Imagine an e-commerce platform's "Order Service." When a new order is placed, the service must save the order to its database and also notify a separate "Notification Service" to send a confirmation email. If the Order Service first saves the order and then tries to publish a message, the message broker could be down, resulting in an order being created without a notification. Conversely, if it sends the message first and then the database commit fails, a notification is sent for an order that doesn't exist. The Transactional Outbox pattern solves this by saving the new order and the "email notification" event into an `outbox` table within the same database transaction. A separate process then reads from this `outbox` table and reliably sends the event to the Notification Service, guaranteeing that a notification is sent if, and only if, the order was successfully created. + +In plain words +> Atomically save your business data and the messages about those changes in your local database before sending them to other services. + +Chris Richardson's "microservices.io" says +> The Transactional Outbox pattern ensures that a message is sent if and only if the database transaction that creates the event commits. The service that sends the message has an "outbox" table in its database. When it sends a message, it inserts the message into the outbox table as part of the same transaction that updates its business entities. A separate message relay process reads the outbox table and publishes the messages to a message broker. + +Flowchart + +![Microservices Transactional Outbox flowchart](./etc/microservices-transactional-outbox-flowchart.png) + +## Programmatic Example of Microservices Transactional Outbox Pattern in Java + +This example demonstrates the Transactional Outbox pattern for a `CustomerService`. When a new customer is created, the business data is saved, and a corresponding event is stored in an `outbox` table within the same transaction. A background poller then reads these events and sends them to a message broker. + +The `OutboxEvent` entity represents a record in our `outbox` table. + +```java +@Entity +@Table(name = "OUTBOX") +public class OutboxEvent { + + @Id + @GeneratedValue + private Integer id; + + private String eventType; + private String payload; // Typically a JSON string + private boolean processed; + private LocalDateTime createdAt; + + // Constructors, Getters, and Setters +} +``` + +The `CustomerService` handles the business logic. It saves a new `Customer` and an `OutboxEvent` in a single, atomic database transaction. + +```java +public class CustomerService { + + private final EntityManager entityManager; + private final OutboxRepository outboxRepository; + + public void createCustomer(String username) throws Exception { + entityManager.getTransaction().begin(); + try { + // 1. Save the business entity + var customer = new Customer(username); + entityManager.persist(customer); + + // 2. Create and save the outbox event in the same transaction + String payload = new ObjectMapper().writeValueAsString(customer); + var event = new OutboxEvent("CUSTOMER_CREATED", payload); + outboxRepository.save(event); + + // 3. Commit the single transaction + entityManager.getTransaction().commit(); + } catch (Exception e) { + entityManager.getTransaction().rollback(); + throw e; + } + } +} +``` + +The `EventPoller` acts as the separate process that reads from the outbox and publishes messages. + +```java +public class EventPoller { + + private final EntityManager entityManager; + private final OutboxRepository outboxRepository; + private final MessageBroker messageBroker; + + public void start() { + // Polls the database at a fixed rate + } + + private void processOutboxEvents() { + entityManager.getTransaction().begin(); + try { + List events = outboxRepository.findUnprocessedEvents(); + for (var event : events) { + messageBroker.sendMessage(event); + outboxRepository.markAsProcessed(event); + } + entityManager.getTransaction().commit(); + } catch (Exception e) { + entityManager.getTransaction().rollback(); + } + } +} +``` + +The main application starts the services and simulates customer creation. + +```java +public class App { + + public static void main(String[] args) throws Exception { + var entityManagerFactory = Persistence.createEntityManagerFactory("transactional-outbox-pu"); + var entityManager = entityManagerFactory.createEntityManager(); + + var customerService = new CustomerService(entityManager); + var messageBroker = new MessageBroker(); + var eventPoller = new EventPoller(entityManager, messageBroker); + + // Start the background poller + eventPoller.start(); + + // Simulate application logic + customerService.createCustomer("john.doe"); + + // Shutdown + eventPoller.stop(); + } +} +``` +## When to Use the Microservices Transactional Outbox Pattern in Java + +* When you need to guarantee that an event or message is published after a database transaction successfully commits. +* In distributed systems where you need to reliably communicate state changes between services. +* When using asynchronous communication patterns to improve resilience and decoupling but cannot afford to lose messages. +* To avoid dual-write problems where a service needs to write to its own database and send a message as a single atomic operation. + +## Real-World Applications of Microservices Transactional Outbox Pattern in Java + +* E-commerce platforms for reliably handling order creation, payment confirmation, and shipping notification events. +* Financial systems for ensuring that transaction notifications and audit logs are created and sent reliably. +* Booking and reservation systems where a confirmed booking must trigger reliable notifications to other systems (e.g., inventory, customer communication) + +## Benefits and Trade-offs of Microservices Transactional Outbox Pattern + +Benefits: + +* `Reliability`: Guarantees at-least-once delivery of messages, as the event is persisted within the same transaction as the business data. +* `Data Consistency`: Prevents inconsistencies between a service's internal state and the messages it sends to other services. +* `Decoupling`: The service's business logic is completely decoupled from the complexities of message publishing, retries, and failure handling. + +Trade-offs: + +* `Increased Complexity`: Requires an additional `outbox` database table and a separate message relay/polling process. +* `Latency`: Messages are not sent in real-time. There is a delay between the transaction commit and the message being published by the poller. +* `Potential` for Duplicate Messages: Because it ensures at-least-once delivery, consumers of the messages must be designed to be idempotent to handle potential duplicates. + +## Related Java Design Patterns + +* `Saga Pattern`: The Transactional Outbox pattern is a common and reliable way to implement the steps in a Saga, ensuring that commands or events are published reliably between saga participants. +* `Publish/Subscribe`: The outbox poller typically publishes messages to a topic on a message broker, which are then consumed by one or more subscribers. +* `Event Sourcing`: While different, both patterns involve persisting state changes as a sequence of events. The outbox pattern can be used to reliably publish events generated in an Event Sourcing system. + +## References and Credits + +* [Pattern: Transactional Outbox (microservices.io)](https://microservices.io/patterns/data/transactional-outbox.html) +* [Outbox Pattern for Microservices Architectures](https://medium.com/design-microservices-architecture-with-patterns/outbox-pattern-for-microservices-architectures-1b8648dfaa27) +* [Outbox Pattern in Microservices](https://www.baeldung.com/cs/outbox-pattern-microservices) \ No newline at end of file diff --git a/microservices-transactional-outbox/etc/microservices-transactional-outbox-flowchart.png b/microservices-transactional-outbox/etc/microservices-transactional-outbox-flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..db9017050e9ea3f5e61199a0a3703abfe5f6406f GIT binary patch literal 33520 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 z zL0y`>_3`xemy4niw}Er^xClo<6WuUvGQGw4XUa#+CYw`&Q7 z5~4sfgM>Z;=Q3#mYPPACDIV#UNuL^)JKC?%)F=IK10mafhk=xDZ*9)br>d*_KZpVa zbfgM@wqm9rP;@-T;f54K%El&=gM|vsT4{I#IKKpS{gEel-he=ALCAPXj3)EB)A7|9 zlt>DbEi-rgKGoz5=7spKI#zkaCNJe51#<_txSWo{*IR%0H4 zdj1zYpu%GchrDP}xaZd1%?(J(xKJb2cu9%k7l^h1<-)g~V;5j12 z=Su5}EIK|~z5Drb4D|7mCP##eh0sc_B|P~4l&ksMV|E*)_?ZSWcoSN6T2sor+`LPS zkTUUFnRg@mb|Oju#f60 zMYyb^r(eu;7X{G&_c?}iSM?d!sxET}1iR*~&3W7KbGmoNvq8Tha|4Y+g_jF^g^_So zb0ID2*|5i=3ikIGMfT$9ZKQJ13Yq9_8|@G{qVPap!?3z3P8nmj-b8WRmJB+t6f0P2 z@zzuEzssw=$Q5JfM@_reren-5kEWraHuaLmWj4k z44xpa;2z{-_F&g@Q>aML9DZHP@L2uvS|`=oSK9mvVo1~|t z7cY)2+H-}M{S!SW*>u}V9lhxpkz?m?`4B}ThRLe~oL8q6p0AjfbP>j!^F00U=;lxpct7-v(r*B^ve%dqA4&fKr&y=13zpf%;k{6l z#v`%qxDrylJo}D|V#sf6p7GqxThY9htWmiflL#jS5K}xlMfDTv=_M+R3^%abvYiSC z{N$G7GgeJlw8S)!IYl2f?3;r~Ze~0VdtwTRc-vbU;gFyzZ$u^pfr-98(k#w`U92y# z?!@)JkH81!Y^oFj7o%hvP`g4Y+HfIh;2tGio#x(rK;gLk`9p^ZoSNRg-3AUpp0TP6 zY0M8PS6d`Etyqp=4l=1bV6qn>g%|o)eu9h`M@bj&@#{)O)lmV{l&(|E6bk*63WCk_ zoY!pupwC|GMFExbU<)`{naC&ScIX;oHc(RmUrRq#~u?fD~N77;9$iO zpIwi71(#~v3?qrwMUOSRLcr*zGdv_H%FLVinBu?j`G11zK0bg7R(R?A)O5zK-$=z( zJ(tZ<;jNOJ%_}Y$T))6Hm4|M+zkf*Z*H=V|K%^n@KI}$A2X~8GdG0~AntM<$YX&Me zOVLc+Tonc}p?pVCdgq#N*l_zXtEsH)vXcmy1@$Xbh$av`SP;j`6QZWLCu&J=Q8Ue?=t^SyFGg~&^cj`Ni}lVmN!n~1 z)vBRJmT`?C)grkTKL!}ptp4FQ?`Y_LSds0K`+^dK`aKICr{*}--~?jQeUOGOF^xO9 z{#T&v1~n6dNy|Bn2lDL{=;NnxGaJSnK7vZ{0GJtL>Gk^XKO7NcbB4?-i#u8@z=;wH zZf5G7l$S&3+DjtGX2|!qL$?e~Zl*xU2QIt&$ zmR=l`|4du1za0DO(Jc)rCj?=YL4FuK!6Ae=;6!1|foVq^AUr-?-zIWH7f3dTqUYjXi@d_<8}6)4J5nHh#95P+Q$aZN82{#lcx(o zR``-~^i=VkdP=13wg}vzLO%a06X#Tg?smENNt=*T$H^Orre9b`Bi_^ZB#?<;2>h~W zu6U0E!|TbKy3F)bHU!^pmy|f^Wsg85~zJKvESG*>-EwGzjr*6Zu z%^lKLQ3oUby;Y!=atSH+;jS$HsYK+vgMNZHjkY%3shZTEHVny#*E;)iczesm{Bxts zcPMeqwC*@WM(JN+q>pBwP2jlj+d)hwYrr$24P*f&res5?yMORoUJm4)*3>Vd7--T>6rxlWQd87%yyV zggr+=2NeqY&BaeIPtLY(bfdvxfEaL7y>e_qxG_T>KnqI?_am0;nC@@(HdNd~|7=<# z@~6@c=X;mzz>yI9 zg*Q`VPd8!^n6{&sLy3mFOoaV~13vhE?f>wn0FFbSd^a>Lp_H@^wVlBe2_uM!wm5WM3)X(z0+uPk${F?WUx$-%%G(Blm}<}(C1((3 z8@Tb?8*7Y@LE}8;+;iJBD;rnftvKoT!DoZL531v71yFp*?(f>Y(%O(`b}!b zjliJ;#Q{9bwc+jF_lRkwpk`iUki)*lj}$ipe8M?!$DSWU+fe4EAo}fp*z4txCm4I# zeS`0(OA-52V(L~k!YhtBG+)S!N26+CuWnZ#{<)pz)TP@Xq;sGGEO^9UwD=>hWnjq2H&Z-a#Wi6S8REYGQb087i4?ARqQD zUgDc`kuK*@_r9iYl`oIw|G=R%)14d7iW-F^my`Dn=n9K-FQQ^>#mpWHft`~fB~Pp@ zL*^^0s^VXCe?G{csp_YFd{Yq1iG4Cjd)_*J1A`RQ3NoRx6hv*R$zl(FPz1X<-`bb` z1F+4tN`8uRIN$057(&KXjq`n3wPdrZrOJCH&10D}5*@}{gsdncEECAGHvvRk!@C1H zYzFXXBjz;qx{q8(%kRU&)-Z?wLhsssJG6*$EgP-6v~6W6MED&+AM%74rqM(ucq@Rc zxCd+$2w~M&G={PO!EFxeCl%S=TZ{1wUf#>O5jNYk8fjk~4cBL?E8dDv9@`(@M1O*m zUF0fNwBs% z(~l#3_)D^4e-$-wz(P&xQLFK1LlL}6gv|=4>9Lm6h$Y+JsN-W9Z$i(1sHdCdUjuBv zTao!K4IDg9K?meW-=R&0JRtmcyj_ThY4eeanP}T8-P=byjW9#+9t3>lrYJ8!UudBh zIYY1cCPRk37D^~$6*ktw3AY^lsfEt(=n1;&FiVb`xnmh1v#1h}EsJxTYYC_YVY0e! zL+?S+eA#lB{_I52q(Yijq5Z7?M!SPaU$w_*;kJ5Hc>!+(uk=-}LHh{o+p|~bTPz;h zQybBduZnZc zNU3D8RbmYBsH+Sc{)_&s-m0PEFt&LDCU$Ib>aF*zn#YfElBuXH=Rzd4XV`FSg3Hp2 z41&1=r(l5NP(L1nxlDSpM&XQRh$}GKo`M8XOxmyv{!;M_v~6fCE^V4?_KDBG>mcLb z80eUF&-9nGUzO@8xZNAx#K@8QgKbq>t1C>9nYfngP0SJ({{p5_rhcx>)5~TQDD)L1 z7>p_YM=(hGAs_ODCyw2IM74LeH8XOK73Ofd zfproQPdbDg$Wa7?-PkXGT?O#Bz0h76w@5!Zb-T2!7J`BL0LO)dtpUO@4PsJfwtjcc zkH)jP%3g36EoMbzNaW-1lt11FcfJZW%Z6#>A94j4wXqrNeg4KeP`cNT#BXR zEDyz;i1S?OW!SjX%ovqPW_JF~qm4R^gXzASd$3v=dfq^!RPmt(ejlvjZTWundh=%e z_+g`}A(GdGYvK4U{(GVBY_?(tKoKVvEksoNOg8}~ob_4MO zAeSI4UKW3%aI?z|X${&e#IR49bxITF znUteS@Iw8XuC{zKcW|x3OzSh=X(n|AygFfHpL+d1cyxtnf=j1zR`M$tP!OBZema1r zIq~Y({|P@6u7^028t?C?O9z*8T{hU0(l3X&PGzR|g6kw`;HQp|?d4@2zhgUu zVT>SxxNM)o=<47`v2pon0?@7&jI+HQ;L0p`Iz&}1 zeOM;mVgYKnjLZ1$x4Hu2u85h-I-+BueR-<0Fy2~hs#?7%*PyI@E9P3ro7qMDsKaNc z?GE1Q>ore?Bf~p3{1$SdSuLy)9mFawRE(F#aa@RZIyDGoKnu&(9KGU3@c3{_{~KS6 z84Zy=<%UO=J1TlVg_jEQgIyNn2i!`W#L;bW3p+;Y=f41|q=pAFzvX-UjD!U%bP9y5zL4JI`gZogD6z4wX0)Lp}POHD7L>dYKyf@kDh1n3KL1$=8)6~n_qp-q{mH3TRgLnOVSzF-@t7;v za9uI-V|LPg(t@n$Q(p6xt5)72GQ5rV!&=AvM$|N1J1MmA#t;pyY9=q3w?VIn7xjxeGMgso~=W^QAn%ElNm*NC=c389vj~{ zx;uAZVqiggkyeygY63;|_EyDYTcST}A{@g5|2~EXIg=C~0HjiZW7YhK`on|n~ush_+Aid7o0@ITHxwYxxA zRMKGkfpty#=)Y2DBY>zy#L;L~ZHL;bW;m<;25E?W&WuL3_v;D-aU0FFl_8!=` zH3flcvNJ0}oaMcD9}}>0bC&w?n1F`Hklo zN!Fww2A56((1vh#FchJX#OJ#yq>1IqOs^?|;DGNq2BIE%EO+WSe+>gaWVt-b=g5bD z5osVR1;7Nj9H25%QAmoW(7$<9!ma4R$V)|KNEOYX zqgfe)Sf(bHDk&Vop8F?0uX}Awa&J}8_-@h&H(lZT0cbzZBbQWw+nc;QH!aIJsv8%Dxi^Qv6vS-f)YfL2K;e~Fi zHvq6(p#xzU;cZ6OOu zN7{4L;VWQWmV@QPQoW}u73Vv_UqWweFiu8Ux0St*f2YVRg3uswrG2U|d}>R9rs9Fr z&cmY6%fvt#s3*KU1ZmdAL)qC(|3_Qp{#LX0&Eh!7z%OzvLP{>jv_4Ti)?f)C8Ywfc z$OLS9hR&xrnH%bHJ-Ze!{QLH{!XG50?ux(h4H@d9vZA-FVB zTYfbO^Kl-Oj`*_4wCI5osLmzCfDTYF62u&+G5hR!wkY;atbYT@zA~6?vka*&VA|b^ zw-9S}G+BH5gT3DLi>{f$ww*T4)jE`aEHU+otw*3D$y7u2UKr3zNiT$}iHbekukHp* zbO{OXMbWDQOUQD=OM$R*Rk*7@ivBdlU0O)Y7;cy~7joPn&B%yX1pVC;%I9c2aSM-$ z?i5BS$w=sLvu^(l1>RD56eZ_`nDq7@)Nh?I%Psjp#z+X`$ja_dX3b7(S2QM&cNBkQ+`ah`qSd?+KO50Ye^fjL9O{sUJ8PMyC)vAMEj_ z|K&RQyW>gY+^T{>C@%J4weS7JR(hC9@;`=#j4Dtp`qILUy@|#8`x2Dm0Gt(eYU}rY zi6KD+FpSsz^vH>K3LSvhd{wy3{(7HRj_|ZMD6c$~-q6<|z+zqWQWUB+&4@XrDwP|N z^f!OD+ygk~Z1)5<7JqRr<#lnVse4c%)wtb@v`-?67sD7SQc2kys_`d}etA++dxJP= z(ZpL)G=FVXw-Zj~qm#e^TyU>8w}xDQ1mR|Yvr$!UZbxM9pMw7IQAVvu94Yd2NI8@H zq~N%iMStI8<#uAM#Y1rB_m2}pu7 zm(Op*eWP`kB>!^)-5igXAx#emSLNNKb`P+HPZ{`tq2%I&c?JnJ-ug@oc$SNK1}Pdu zEjIgt4JOhIWqLEzlW&BwJ&Z*xe;kjXU$MgW%rh$xUGQMa8RAQ?>Z*dK@kQ8%k>Dd9 zcofI0k1JZ-Q%3fba;di39rCX=QdNwIVtVL0DS4KZW#jo$93&=@;vkQeRj3sSh|lEt zL%)b_z*lVs68d*Yze47NfCSF~DN3hzOSdWzZ&MZj_qZt7&_9n?Y`Ux4hN`;i(gAs40+|t7ZA{146Y{H{YOkw z3d;y+RSnNs#k-73D#0Q$mf%l=Ei%XnU6I`kRUxQU0HCGDIo}mg(hMpGe_HQbpiX1& z_tFYN3(B1d){nn8hqrO)$C{xwgHGLOMRw03Xs$L2Fd^8MV5Ti1|9;iR(Lc;%;oH3T zc7^_HIzbX`C2o7Nr!o#J@P3c}ErT#(7*d82It%W+Rcl^_*~cyZmpk0P1z%>(O~6@e zou>Yt;-EgPg2=AmPrF@A7CP0Q8d%|eM%~*ii5$H#^b+xrI#(Y|Bld&LJ1da~!XHLA zt1Gw4OmJ-ivS_?pFbgNmk@N4pv`MHn%Ry4?C&6e&uR*0l)rs}czNdN(pv_uW^A}qY zTyzsH`WGHcZ2L1w-^grSL(JoIm8(W26w=Rk1e(_)P;Ui5gwlf2%1?vb^-j(lY;qzt zSRz0N`#CKNgLJYOrq`Z#=`}b#+aOCTF*0-8L|u3mV^aYsBpPssFj}?yHP*xh;(4!E z3xY8#VsG8LfL`pxsfy1E>gVK0IQ~E^Whq^W!>Uc(f?%T0@a##G+{fUdFzAtmiyKOg zUg4GzIkHsE2qJmjN?No{L3|S^a~9)TCf+8{q_tzUfm5ALjakfm5JqdwgzPKI)y~M) z6dz^uy=Bk)fR_5JH3rQfaFnzclwI$O;GW!Sk@F-@S0dfbMBQO2a&`~90F$bU7u5N7Av>VDKrT1 zuStRq1|7s!plx$omEgxvb>$PDrD8ucJ5|g!e!k= zbK-*H@fY3TsJ7zOT@^nOaiw@wb5Z1ys!FIg%sF)<6{BGt)>d1c>nKS>qJ>7~jd zKnH!RFIa?I_#!N!nq};{+)21t{)phBy-@l342ZM+MHZoMxfx@2YKNhdR4ed8!u-`v z++26AF~%t0{O^rPrveQ|k`LCwv@?+8_vJ9NXFl2X@r*Mch5D^R-I!U_xVq-N*`k(6 zvqqPfBs&Vq)if9!(Ioqg390%*mohip(ni~@rC1H6$yMLpUxs|zYYjfqI$o?a@yEm& zp~m>Qk*TD6U_7)@pSjN}e`BXTWI4`J)ABnwxL3Uh*Q{lQz1CMuGFd7i(+tKM%19<} zlPI0`EE%~hTs^m{&leiC`mBn)Zjs?FX|WylqJ?+C7ng#Zr;ed)%2>nX`Q~cktZnA` zvlOL9Zj-0&?*sW_$J4+G|H3tL+UX@i*C(JAJ;dQZBwMv|}vScR! zwsm)1^?=p^Jkmaw<(^JOUg0utbo+G$Ih*TyZGD}{s9HJRtnKpzT1Bv-Syvx%i()Od za=qa#9vObEgy^{R&;nSP9nv54#Br%4>Cn-^zz~s9foCaTD%p^nN@XiI?_VD(LfYeG zbGm=KJ+ZW<{}n@sNT`)7X}vJ`bxOS5P!b+8G^i@H1AS6cDWlH?d+N;8W}Bh6>kVuB z<$#R&OM|7;B!xNs^b@2o&EuB600aYsO(r)5!4A3ie&symocn<{iadD*z+bZIu(>je zKmPdG_jAiqloyNNUf!bv>dc_oDa1DtUxE>bAS8eMNz}6}#&kc5fO2S1rmD<3Uu3wJ ze6ZmfZNYL|7qKF{F{RqFQ-M%tTyhR>@73JA>XJ*E*Ue>5I_1e6S#ZgqCo-!8Sv?=IbzChwMqI14v52$y$sCN#^viY7oDO9`>d~r)3 z6qr8LgidmN(rD<*Ntkjuk1I>a!E)%mU8~t_BJ%2Z>3|5C zBMGf4!i$p`V+kv=#pevQ*0-XW}BH7joVM^4OZF(_dRB`bv|%-X^x=g*xp3|WH;ThH=ie>uafaw9PK zRK#FB%&6%0y4hv>>JapB)6^0 zozX_&S-zl!=O*RzXQ)%4lJS(86?UftISN^HUvb!ZbQ(Nak@Tu?V zrW_la`@1&+uW2c~d@i7Lt}`Q|+`e-@an$kUF|UK6)=TPx+)%{N#&s3dUR{oGYQE6W zqZjJy$duBnAVC^p2>h+gChMR~5?kCp;hq{73$1jhm194%3Q}oOb5e^XKHvpwGK-2^~085ujI$l)+l?M-KN|>_?UFV=F3K%OcE}2KJL6{$t1J#$#yen z2_h?}ect(t?`GVtwMpi`Jh-5sb(o(XT}WZPmXd6Gj`AC)Mo9YU$K|(ZHeP=&dOLAP zYZQAu4pt>?bYQl^@xt4eB?R_pzH^=Ll$IXq-VWKwS1)AfWNs7k3#^7!Iea!qa}5l? zecQ^87ml!%wZU-{dBN9HEG6TUh`KnSe0Zy)-axZjrCu{ko%07J-8Ipv)GZ^1eHe+2 z%>!&lMlNn-_{kAtr)BD(0nV0Rh=ChxYNB?wpPH^P$c>Ui7{t4=@@%Y-HO*;aeD1uY zM4lRS`CJ;#6jO3e3#kOM_B-2nTJE{j1YId8y#FB={MtNvif~`uj#msxT?PmPb&0G5M$$J#h z9oj*ym%q%Dd((e+Whk&HJT0-0!En3Oq)fRtN=VU@D`0Z-^7IWCqm9k3;RkK|da3D_ zOERR5yfrBIEVCw1vb}=ry?4JBh5GiBa>lOJD4tI9I5wZdoF~{_4J61X5twfo z!L@g8FxYth#OOz6h+a_(x=K?`D5I?5nMl zH?p}TQfVfnz{4jzWT5#m+#l6<-uh&T^arJc7YkV2VkD?T;KZtXR*poG(kaw1`@d$EWA@R|7Wm?+=GIy|&H^%a~6&?-`Nd zY@c_XT08Y|fg39g9pqydcV0%q5n6>8v{fab{BN*GLc7b^CF87lt-(+*vi!Q;nh4-@ z-$sFE2sl>Gm1HC=jmQgs>fNwZ&r3*8r`z|SoSPRGuVp!y%MI>A==ustKu+M8yuMs#{CD?nB$XCR*kRE$RB;j3FSK5{K)|yWKJFD$d7wVhGJ-@<_!l+AC6BO zU3IMdQNV6_(67V%HJ4G{46-~cfnQ{xZ zz9at#Uwl)Fr_Ay{kE>FAn^5L$Ah_Tg6Msv4rn=*Saiq5Fjt zhhgNo_X%gk(Q$unrNiD|j+mY~wKmnDP)zQZu7@>D)6=5c_w*YH#~$o;cELaz@~0aH zXfS(ICZ##zoxVy*Flga>4C0a)FC$OBjTVdOaMI4L=H<dpj5ccO%>*58l2pn?>IJf=3~RUfr?dYokpFe|rL?T%3u zuA$tdf3Z*IE<83tQ7`ZRYwybApYVCOQ5i}h<+Kc%oI)x~Bua>keF@o@60$EX z!pPD|hbS^x#*BU6bz;&&$dF}_Q8NsZ80+A@o;s&G)o=N{|GuwJfAo2tnR{mL=eqCv zy07nYXPG3D@(t*yZwsp-|C58#YFqJQksCb#+@ut|zY) zAvCWBAj!xwN_?L*c3%~WMVPl!N*YR zYzyZIgy_;|Xwn-YN&hEFK0?DqYSqyPA7Hf#eqPI=tFGG3_TeT?=P%_qztP}VnmKjU zI7T@40g0Ac9CGL?uYwrtPU8ybEAHJc~5=aRVmY>o#X$qGn6QSz&AW}=MoTCI(2 zhn5YdhOc;NPTE(Cg%bMmPh7CIS#oaibMC|u(aQ;|35>%4NG*ug2;c}JL?$c6;to2^ zCivKbZPxP3FQTq1JBFrC#Y&A#1Rtx~yLO1;mN6Yme|=g+=hjWf8ns)T^+x^r8|CWv z?I)?(Ojft^1T;oHxRT)W!68y%?QB~fe|X<-50ot1^r|RU?v+f`0qbWm>vH6YN_fsl zfoGX+4`^v-A@balM|4bz%Brwh%iK*G!Uy)Z22_JUlWl7=m0GqrVJc^UqYJ+n2Du%{T8PT|R@I%!W$zgYSi;zI>a|#Al<8dw0{;m=M7Lh{$!m~(L)=PT^pBM6( zozAJK+>H8A*!4%XF6ni&oOI2Q`aHl&e(QHA$hsIuHJtjdv{3va!Yhjc{tLcYl7uwp33_eJj>i~^eW^vG*nL-W;flWF-#gX=A&4FoFm$i4MDs`W~$8Ky)-j@bn2=UU)1KT41;tbaqvHI>OAS?%sP4Uj#-J1@Y|5J z8EOVwkEdL(q(4fyGx4A|3>}1*r*s!@R_?I1L7hNNQda0sct{Nw*xE#vh`GD7w6$}{ zckYbkBTuHmph0@H;hj2{;?NbqzdBL%^iGGi2}UZlT&*SD5Z)k)9p1 zEBAreKK$q$8uO8D*GVhxo$2WBTh+$;EMY!1ol0Amplcg9S|mbCAl@A9}H4 zBWb;%&W+v?hUpWWhuOX&^UFdtlwWrxv~mY&#E-cFl^or>hQm%7qN?B}{ng-d4Ug08 z-%ROg3)XHYj8#PP{0`ORKDgm!&wHxD5}y{_!u~L5bf_e^wnjK#I4G|0bqKvG`PGJK zp~mTSm7@pT`9b(x&_#cBft+0HhYaggr7=IYr zZ0;l3!;9Pym3X-KMXVaduM5G6j4i`{Wp8aua4x=MeqXFrPeePMX0PcW(Jtjacv=Ry z-h9*PY7G8MF{m6T$>%hNn*RB&dQhP1Q02;lUSLMvkA;n_+Qw&U@vgfh$6NxX7_X_Y z;I6QB9elfc_7jeax(oirYR3BKeauiT`KS?zR!a}`PfQOG2x`8o+o&~J;`l8d2%++(utkuV(1VK{Dd+qAm89ct`;D>xO)3pwh68a%u! zeietVvTZlpDCI=e5EN#z^~?(o7i93?ODxH1V`e!x+7)p6p?T=Q>1YT0or@8DShbO! z3eWK5o}9`LILx`NK)P6*_7fR@5G8*RYPU6*lXTFj>bYbUwL7>PJGci}sCJH{9pefT z3pR88(jN9=KlE0_$SOy1q5XF&fJN*-pcWy-Vc!~?4vxe#x5bumIEQ5VDGsx{1m00FH73?4&O-O!6SNY@!WV%{NjHHG2jSFW2Ingws> zD~PKW}lmqr$dfi=M7 zN~U4*&O`D~95aEE-G$a8!(f#0-i;$W_Yr4+Iqn0!EU4`34TdC_gah#luEIS z_P^^Kmw#|1ZyPx`-L}@a1xrC5HxM+6%Gjy5iDNLw% zaffAf-0oQ={N4`W!Z}sX+)T&b#qyr;=2h&Ry^-BE;w1;R9*dS!zs8!-LKYoco60q& zlfC2bZHbdj;|e3l`yXWNs%&c;1TEX_qvb)9g5XEv{6hOdvJ*$pkfqty^hV((#QLgf zT3c1{EqH;_xO|wig@98D$4vJ$%EWU}YCn#0H+#Y#^4t{laVV^z z<=1eW{w>@&VB0mn;$Sb4Yt&q_yGRR7^3_vtp7GC9Bu&__Rt-3~GH6bFEF3I<=6b`7 z$_ftdIl@7~!n#wKbaTUpPCi;N&&66k&amB$z||NF4^^1&Y@#A$>B=pNYwe3(h$vs5 zX}Z26@*v(zhJ&I5zGH33O0(%0}I|}|@ zI9lx~w3u0LPA2trmC3r+;JUb4MR27G`mN3>q@K!ux;X!XteCvrxi%;?gnL#1W16DX zlQO?Y-rdA-{EENz+m&N)FG4r!AvSvgZ7$N%#SSh$Nok=?!qV{np#*Z%Y#e{RM`Vz0 z%vQJO`MR@75g}+~x`RZUw9;`Qw|iHdI6XZnj>$d=QVwPdt|vr)%7rUb3Fy_(#OV zWV|r3wIyZw!p*XGYbSYHCsPDkb_*p2cM1YdK9XJ6liw1~la||K(^qdsSdgH@lzD3}`B_pthe-qC0 zT)g1afOk9S&=t;8!GH{@fgcI%;|`oro`$`tv`}l?m*1pZe?lZL{KC3c4ufO>HI4exBIbc@G=oF0YRI+Ta z*d}N(z7_9ZcR()V@qxBix6w3fuJQa}ucD}lt1%E{v$Wcj6St^98y}TloUsZ_@>#Ik zO#_h$^8LY|c~0ayxV70?`2sHKmq&xwc_6Oe2>^BbpHD{g-EHq{H}{xN0)2ai(I$B= z+$2&+u1lhZ{UKQO5QrVRrEuK7BY7dh#5)R_&L9V485@4-;aYc*JcoLdqJg7~_1}a| z_&Cnnl}ejkrUc{AZ@eG;HzsSjF=cNy1SauWcL27%0N?(q&*oXotIMOIJ0jmlospz5 z6qd@yI#ar_Jf(mwrH_?fojwn~1pyt_AQM^G>IB*dCthLQBb(&?yavWM3%cp~ZIX5} zAPqb(W=B8GPGhX<($RByZ?-q2PZd%O>|r`lko!lIigAFiEmb38GTZ68VpLdU4D&Qn;vAzc!F7ufDEAa9tG9B*i00Zw-T9X8{e zitI~mzv?*UoLbe)%!ju#{!Q;SNKXG8la=YWmruBGjtLCfcL6Mt`D`-o4();(YxhfB zhPTn1e*T)7U|+=e91)~~G4cf8wBw8Ya?=3(%fL6hb+SHHw(Fy{Ax7;Sc@;`wa)xGA z=-{O1cMyDO{F*YILTwTCPk?r4ehdoJq5rA-B72-fBC+b?oGlx&2 zkl^dIlr)o!lBHJ%<=?*DM23gB+n$VNoqfiq6m2po(`6^5;qJ;g1lp+%I`V0onCygHRCo$@y2z&tIHD7-D@WA}oQ`nvHFgf|+| zjU%tp&Qlc2Vde=4l|dR&0e0nh*BU;rX8yn>6?ULZ)w-I-z&kElQzwQl8s`7{+Ntwf=zD`?E_XU3y0Lzao>f%sY(o zmvv?4Rm?)ri~g-?A>Rb6yQtYLhTEg{=EMpE?YDmI=qyMO4aaS?sv-~~vVv9uU(oLO zmIbiJ&aTm_BK^knPwA7I90$tLRLRi!g0feTgmIO0+9XH`C22>cjf`89E>6&|JTw&yDL6 zZQ6ru4!)aBSv^4BSgd5j@f$AGG_qhdVAwKxBxgPv79&thN_2;nE@WvMh&Op-B5`Aa zaUwECq&eTte-m53N5y6j_5lmZ<(7uDE6h{r!tp8d4A?m7>=McO?Srh~v-7OeEj+R; zAML2yK)rc{D9Wm*Z_s;QR+=zV@tAjr05Qo;edD4dN}F}9g=z}Kw8$|s9xE@!A4l(a ziC+$nu6%6s1T4#QsN)4@Qp}clST^T`jw5R_aH<$opyCXNytZd)H?mlOZq3P(g+Sjb z1O7-zA$PV-duco>u(W0gx;_0Rc=yo|``c+q+RI>{-Yf?@rGRz0pbG56guaihVUq(d zxAWUpMX^%nKn`{hITWA*A(X*G(v&YbDx2JY(P$w^Y8zx54gY!210B7D>i z52>Utbr;5kE-DBCeLoO0T%i6WqMf*CCT*;i*(Iqz>ecN1%>tn^uG4bNZJR{t>nn@u z>e8#Ui-*D^^qeTy7%-(LQ~)2Z2ShUqZy3Pf{ZNHE52|NR@XY#{wj#do$eA=Ah+{+r z=pK6@d3~E*`QfA|Vp9V1IJL>z9jMH6Ld%h30v4#p_)Xn!CYcXG9^(nr^Te^>WTBhr zv&`phkS#8-Pk{Sz+7>u);V=kidSIJCM~;=L;T!-R^oZt>@nZ`1<#dgg%m}167aXE$ z=6lAdGvfn2h(3#9{PNnks-Q;}SNGe9ai)9p)ox6~8lgfeQUM6^{C*zrJPe!~m#Z`~%H^%`GPbG{LS#uZ> zv6n#IXFhBUI$J{_gd*UW*{^5r?@`m0wljTcNGp)3XK0No0Xo{e?1KX#G4$JWPqhMQ zNHU~nVDWPaERq%AQ1~M7gfV1e{!wkh3xleQT;1FO?^noNc&%7M(NQmhz17RLK=xvd z_4WCKn~&$h9BX3Z)HEEsfS8D}-t}}#0HetzkId%=I5c4xi1fx@{lN=tqi4#Ew^DCQ zNx239HE+ShFn){(9)|%g;%_rJd(aof6bj6aFaZmXjGr{0@I<*8e%{p{_Sb$jF*rL} z=@T%jI^P;l6et)Hsal^p(CW(c@}R~ynCB?xsOG5WXyu&A(a$l?F=rfQC^J+Ts*Dp1 zb%rwp*c3yXafX3q=rar%#tc)2x$Eg8y!>QUSDi}U5(vt+dhKyt6O=jGa9ody3VgdP zsmVehGNb7L$`s4%BJIETty^rCP=b5{uEf5T@yB8#eJiEO&bY1jIc*?r;jBhmWSK+# z)fYR`hE7POWmISDu7}hxRst96qcYTIsTJzfI*PtT*qus?!Rey)GVD@mZIH+MiKwec zph|S#=;W!ZU{E!>Kb?_0r6P%{*A+dJcS>bHstNMg!{%ydv^B_5s7_s{Gc6iq8C1XS zjWgPsWLd#mI1hAJhF7}KYV?~7LOLxG=ZBuk7)+-n;eyaBgwqegZ37zjoxtGK|2sjGqy7}+urRD zPecb0R}(D>I6OL%s7AERz@?!Rf%Xtw9Zkd&aU^s;v4e;w;d0QAh?_|GOk4r_CDDL{ z&%%`;IvC6wB7ua_LVqB3knlOU8fI7;Mwi)$7jj@`qzO4P%hH6Lm`~GCd)o>$^QL)XO1iZ8_~l(xBYLn@NYrY5x3h!l3_O}jqG5t7__KORSIh< z*XP$^{6;s?E=KnmXm6kD_Gibx*;RBw;6!D-GLxnlR*6t5ve8|K==pWXz7qYm;JW!d z=2e+St&EhYiQEkj*9Q+M33`D^U7QnG9RIV}tgg)739MOBS=MwmE-AkHXcepCO4^zh z_jMWX8GGiJ-TOfnV~;Tt*g#Cq5qOkok&azILG$uutjbM0Ih$#Djowb-VLP=(`)i!P zZ)ObDNH;~1Bay-T|v6oJ>-7dd3%v!u)UP|tX|!s5?pa!J~woKd1uCPrMl!>*rl zBs)r6h)Mt17pggY`-nI+nXBz(19f$kXHn_xUN5THJung3fS)-2Re^)pq z<>c&E^l`$>{pGd54{s~g5KD4gC!JNE-PTalRY7-YV-Dzui#mk9MfUvq7O&8ZPKPnW zac#k$Vb*7j49TGm+9dg3sh3Wk{9!{sZamgg((CgaiZrf^{=_}nZ(yJKzTO)& z=(AV)!K(UAxKH}si?|xoV1=qAmY~sC6{y^05;5j#Y-AS=b|G{=BfDh2dmp#WP*YK; z*>smL)zIdSH9LCPDF8cq9&9H*M77xB_zLX#s>}+4>xhDGZIP?)seTjT;*Z7XWY=)U zs?`}f@}y147SZT+Hr=JJ1AZckJN%(8A-i;twO3dBJ>>Duog%YJphXc?tNf^Yu(uvQU^eQneS>%Xrieo=^1i05vxG zNe%!CF;U~#aSm3|9+bYx&85~fl1}AmiN=PtRh}%fwhM2Z5H)iTs5us5eqxI3HWblr zU32bT<%DKnHzIB+)tEFu?b3p+&Qb*@BF?-BzDly7(NX|MDn6Bo2Vx@E${NC+v#h+yW62 zkKqNB6l7*MkfSu&O&~|87$XywTiyOUQU|NV(C;(!@B3_k7iwrkKkaSmd_Qnx?a^M? zo3WeFqD-aBJ`%&18qJA}1~^(Nv(5HsF;p-I_n!q*HG8(k{#mQ%0l7Q>^R^SaC#hTP z_!@Bj!(?tpa@hI>=;t}_4qO6Vw}F2{+X~l2Z@Md@dO(hU+CmOlwqoUs*_k^)F}*KY zY3Q~adiF)cJQ06#T&d{5ewi-cF^Bai!EP`u?A1_e(Ypyv5q~2V{k_-kRY_tGe>Z2D)k#Dh_*pKn9X>f$w)v;bbpzjt89xCi^IMO)6WFxvO~PM(}8*MR8X(Qn;+$*WOpfUfqM zi}Iq@fCPEecc(p4EvK0m6O^i>y+E)tlT@TM45O14B7Yu?@&n>huA359Ssmxfw@VHM z*y+F&_E%Wxh^cqVVEN!vDTh2-Ac4M5cfB?#!x8IYPm11+h+C2_y(W^fFCs-M^S6fl z>mbVwemtLR0E_yvMn@v;K6|=miKR?n^#&kC4`lz((5F9I&#hjSdyO-!++1#zN1o!eqb3EUF3sHPFJL*thp+Hq z!!sTZ3`B%kaOlNCb&>s{YcPeEj(c&2nTjH@{zRVI+0j9)81j$bE+5zde`)740$zBc z?(NuyYM(Gvd4!zJ#_1==owGjyfl%j;=AWP)9JNik=9j4X^BNRy(OKo;ML&8AlS&~s5ja_r`3h%(m8)A|#N@PpUwEq;1yR|S-YQDa2jPc{GVYwI7g zsgHO-Yw$7nq=)5Dfw7<1@#~uci8?NkH`gV2H^5-Bp^dZm$kGZtU*E7Bk3$-%MA}yPuI-(;&a?=m>i5lOet#XHC&`e@ z@DUu=p`GiZ{_!5T2iJoLa+0Hw?@9T|IRJY73z7o4 z1rJ<*c<2WZ3v#@#H_~>%@o(GK_#4^y<}<&)eu{sC)w&hX6&%la3Iv4^H01Mp;2v^g z_#=4sANpEJ1UX=c@Jkn4?Uz|6u5qQ+WftK>9}ClPJ;;Nvo2|I|s~dQ-6><#k2c+$V z8{a<1ucGTOOzOL6`s?~9tP1JR7!jMpM?OX5mwU*Kc9>2z|5INFD{Y6Dp59nE`xPMd zIXF3n#fxVz^L$3CATq=2k@H^PAZ^cY{`NT@(Ebae+VMA5_v!h5e_hg4?S||cWJK*& zZn)%yz*?W*1NV>{nE=7x{W`9H)Yn`e$IAVkc3%&aFTvSv=rplHgu+3}azV>;xf*VFhY@qFJ7^Ll{FJtDfw!f;{ogGBNMBd_aKVRs-`AIh z+nwjQW@@qw3@BsNriHKn>Ak}wO#PB_)BkUUY~K61d?q3};x!iqxV vm^a(m_Mb>^a`XQFORdUtKyz>mW-cxBi7Iu%l<&bI^T-Jm&12b0XRrJZ(c@_9 literal 0 HcmV?d00001 diff --git a/microservices-transactional-outbox/etc/microservices-transactional-outbox.puml b/microservices-transactional-outbox/etc/microservices-transactional-outbox.puml new file mode 100644 index 000000000000..2563dbc0f3a6 --- /dev/null +++ b/microservices-transactional-outbox/etc/microservices-transactional-outbox.puml @@ -0,0 +1,69 @@ +@startuml +title Transactional Outbox Pattern Class Diagram + +package com.iluwatar.transactionaloutbox { + + class App { + + {static} main(args: String[]): void + } + + class Customer { + - id: Integer + - username: String + + Customer(username: String) + } + + class OutboxEvent { + - id: Integer + - eventType: String + - payload: String + - processed: boolean + + OutboxEvent(eventType: String, payload: String) + } + + class CustomerService { + - entityManager: EntityManager + - outboxRepository: OutboxRepository + + CustomerService(entityManager: EntityManager) + + createCustomer(username: String): void + } + + class OutboxRepository { + - entityManager: EntityManager + + OutboxRepository(entityManager: EntityManager) + + save(event: OutboxEvent): void + + markAsProcessed(event: OutboxEvent): void + + findUnprocessedEvents(): List + } + + class EventPoller { + - outboxRepository: OutboxRepository + - messageBroker: MessageBroker + + EventPoller(entityManager: EntityManager, messageBroker: MessageBroker) + + start(): void + + stop(): void + - processOutboxEvents(): void + } + + class MessageBroker { + + sendMessage(event: OutboxEvent): void + } +} + +' --- Relationships --- + +App ..> CustomerService : creates > +App ..> EventPoller : creates > +App ..> MessageBroker : creates > + +CustomerService --> "-outboxRepository" OutboxRepository +CustomerService ..> Customer : <> +CustomerService ..> OutboxEvent : <> + +EventPoller --> "-outboxRepository" OutboxRepository +EventPoller --> "-messageBroker" MessageBroker + +OutboxRepository ..> OutboxEvent : <> +MessageBroker ..> OutboxEvent : <> + +@enduml \ No newline at end of file From a121f9337a6cd9c94791a27553e7ca92119b169b Mon Sep 17 00:00:00 2001 From: Tanya Johari Date: Sun, 5 Oct 2025 16:26:08 +0530 Subject: [PATCH 4/5] fix: Apply spotless Resolves #2671 --- .../iluwatar/transactionaloutbox/AppTest.java | 31 ++++++++++++++++--- .../CustomerServiceTests.java | 24 ++++++++++++++ .../transactionaloutbox/EventPollerTests.java | 24 ++++++++++++++ .../MessageBrokerTests.java | 24 ++++++++++++++ .../transactionaloutbox/OutboxEventTests.java | 24 ++++++++++++++ .../OutboxRepositoryTests.java | 24 ++++++++++++++ 6 files changed, 147 insertions(+), 4 deletions(-) diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java index a0f695b75214..2adfa764b02c 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/AppTest.java @@ -1,15 +1,38 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - import static org.mockito.Mockito.mock; -import org.mockito.junit.jupiter.MockitoExtension; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class AppTest { diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java index 9b1741ffeb71..f252f21ee46a 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/CustomerServiceTests.java @@ -1,3 +1,27 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java index f18bfdbd9c5f..dd206f71421c 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java @@ -1,3 +1,27 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java index 8074e329c395..d4774b3b389e 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/MessageBrokerTests.java @@ -1,3 +1,27 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java index 17d26a29d44f..498b6a89d060 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxEventTests.java @@ -1,3 +1,27 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java index 9202ac0e6799..d69a2076f681 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/OutboxRepositoryTests.java @@ -1,3 +1,27 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertTrue; From d840eea5621e2349e61f87527a93ae56ba51119c Mon Sep 17 00:00:00 2001 From: Tanya Johari Date: Sun, 5 Oct 2025 17:01:04 +0530 Subject: [PATCH 5/5] fix: fix test Resolves #2671 --- .../transactionaloutbox/EventPollerTests.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java index dd206f71421c..94ec680dfff9 100644 --- a/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java +++ b/microservices-transactional-outbox/src/test/java/com/iluwatar/transactionaloutbox/EventPollerTests.java @@ -25,7 +25,6 @@ package com.iluwatar.transactionaloutbox; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -41,7 +40,6 @@ import jakarta.persistence.LockModeType; import jakarta.persistence.TypedQuery; import java.util.Collections; -import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -176,18 +174,4 @@ void shouldRollbackTransactionWhenRepositoryFails() { assertEquals(1, eventPoller.getFailedEventsCount()); assertEquals(0, eventPoller.getProcessedEventsCount()); } - - @Test - void shouldStartAndStopCorrectly() throws Exception { - eventPoller.start(); - - assertFalse(eventPoller.getScheduler().isShutdown()); - assertFalse(eventPoller.getScheduler().isTerminated()); - - eventPoller.stop(); - - assertTrue(eventPoller.getScheduler().isShutdown()); - assertTrue(eventPoller.getScheduler().awaitTermination(1, TimeUnit.SECONDS)); - assertTrue(eventPoller.getScheduler().isTerminated()); - } }