diff --git a/persistence-modules/pom.xml b/persistence-modules/pom.xml
index 20ee368827..7512e99441 100644
--- a/persistence-modules/pom.xml
+++ b/persistence-modules/pom.xml
@@ -108,6 +108,7 @@
spring-hibernate-6
spring-jpa
spring-jpa-2
+ spring-jpa-3
spring-jdbc
spring-jdbc-2
diff --git a/persistence-modules/spring-jpa-3/.gitignore b/persistence-modules/spring-jpa-3/.gitignore
new file mode 100644
index 0000000000..83c05e60c8
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/.gitignore
@@ -0,0 +1,13 @@
+*.class
+
+#folders#
+/target
+/neoDb*
+/data
+/src/main/webapp/WEB-INF/classes
+*/META-INF/*
+
+# Packaged files #
+*.jar
+*.war
+*.ear
\ No newline at end of file
diff --git a/persistence-modules/spring-jpa-3/README.md b/persistence-modules/spring-jpa-3/README.md
new file mode 100644
index 0000000000..9db4527148
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/README.md
@@ -0,0 +1,4 @@
+## Spring JPA (3)
+
+### Relevant Articles:
+- More articles: [[<-- prev]](/spring-jpa-2)
diff --git a/persistence-modules/spring-jpa-3/pom.xml b/persistence-modules/spring-jpa-3/pom.xml
new file mode 100644
index 0000000000..a249de33ea
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+ spring-jpa-3
+ 0.1-SNAPSHOT
+ spring-jpa-3
+ war
+
+
+ com.baeldung
+ persistence-modules
+ 1.0.0-SNAPSHOT
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+ ${spring-boot.version}
+
+
+ com.h2database
+ h2
+ ${h2.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ ${spring-boot.version}
+
+
+
+
+
+ 3.2.4
+
+
+
diff --git a/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/InvoiceRepository.java b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/InvoiceRepository.java
new file mode 100644
index 0000000000..b19ea156ec
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/InvoiceRepository.java
@@ -0,0 +1,128 @@
+package com.baeldung.continuetransactionafterexception;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.baeldung.continuetransactionafterexception.model.InvoiceEntity;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.EntityTransaction;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+
+@Repository
+public class InvoiceRepository {
+ private final Logger logger = LoggerFactory.getLogger(InvoiceRepository.class);
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Autowired
+ private EntityManagerFactory entityManagerFactory;
+
+ @Transactional
+ public void saveBatch(List invoiceEntities) {
+ invoiceEntities.forEach(i -> entityManager.persist(i));
+ try {
+ entityManager.flush();
+ } catch (Exception e) {
+ logger.error("Duplicates detected, save individually", e);
+
+ invoiceEntities.forEach(i -> {
+ try {
+ save(i);
+ } catch (Exception ex) {
+ logger.error("Problem saving individual entity {}", i.getSerialNumber(), ex);
+ }
+ });
+ }
+ }
+
+ @Transactional
+ public void save(InvoiceEntity invoiceEntity) {
+ if (invoiceEntity.getId() == null) {
+ entityManager.persist(invoiceEntity);
+ } else {
+ entityManager.merge(invoiceEntity);
+ }
+
+ entityManager.flush();
+ logger.info("Entity is saved: {}", invoiceEntity.getSerialNumber());
+ }
+
+ public void saveBatchUsingManualTransaction(List testEntities) {
+ EntityTransaction transaction = null;
+ try (EntityManager em = em()) {
+ transaction = em.getTransaction();
+ transaction.begin();
+ testEntities.forEach(em::persist);
+ try {
+ em.flush();
+ } catch (Exception e) {
+ logger.error("Duplicates detected, save individually", e);
+ transaction.rollback();
+ testEntities.forEach(t -> {
+ EntityTransaction newTransaction = em.getTransaction();
+ try {
+ newTransaction.begin();
+ saveUsingManualTransaction(t, em);
+ } catch (Exception ex) {
+ logger.error("Problem saving individual entity <{}>", t.getSerialNumber(), ex);
+ newTransaction.rollback();
+ } finally {
+ commitTransactionIfNeeded(newTransaction);
+ }
+ });
+
+ }
+ } finally {
+ commitTransactionIfNeeded(transaction);
+ }
+ }
+
+ private void commitTransactionIfNeeded(EntityTransaction newTransaction) {
+ if (newTransaction != null && newTransaction.isActive()) {
+ if (!newTransaction.getRollbackOnly()) {
+ newTransaction.commit();
+ }
+ }
+ }
+
+ private void saveUsingManualTransaction(InvoiceEntity invoiceEntity, EntityManager em) {
+ if (invoiceEntity.getId() == null) {
+ em.persist(invoiceEntity);
+ } else {
+ em.merge(invoiceEntity);
+ }
+
+ em.flush();
+ logger.info("Entity is saved: {}", invoiceEntity.getSerialNumber());
+ }
+
+ private EntityManager em() {
+ return entityManagerFactory.createEntityManager();
+ }
+
+ @Transactional
+ public void saveBatchOnly(List testEntities) {
+ testEntities.forEach(entityManager::persist);
+ entityManager.flush();
+ }
+
+ public List findAll() {
+ TypedQuery query = entityManager.createQuery("SELECT i From InvoiceEntity i", InvoiceEntity.class);
+ return query.getResultList();
+ }
+
+ @Transactional
+ public void deleteAll() {
+ Query query = entityManager.createQuery("DELETE FROM InvoiceEntity");
+ query.executeUpdate();
+ }
+}
diff --git a/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/InvoiceService.java b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/InvoiceService.java
new file mode 100644
index 0000000000..e1db310d03
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/InvoiceService.java
@@ -0,0 +1,27 @@
+package com.baeldung.continuetransactionafterexception;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.baeldung.continuetransactionafterexception.model.InvoiceEntity;
+
+@Service
+public class InvoiceService {
+ @Autowired
+ private InvoiceRepository repository;
+ @Transactional
+ public void saveInvoice(InvoiceEntity invoice) {
+ repository.save(invoice);
+ sendNotification();
+ }
+ @Transactional(noRollbackFor = NotificationSendingException.class)
+ public void saveInvoiceWithoutRollback(InvoiceEntity entity) {
+ repository.save(entity);
+ sendNotification();
+ }
+
+ private void sendNotification() {
+ throw new NotificationSendingException("Notification sending is failed");
+ }
+}
diff --git a/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/NotificationSendingException.java b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/NotificationSendingException.java
new file mode 100644
index 0000000000..8c2fd3cd5b
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/NotificationSendingException.java
@@ -0,0 +1,8 @@
+package com.baeldung.continuetransactionafterexception;
+
+public class NotificationSendingException extends RuntimeException {
+
+ public NotificationSendingException(String text) {
+ super(text);
+ }
+}
diff --git a/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/model/InvoiceEntity.java b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/model/InvoiceEntity.java
new file mode 100644
index 0000000000..bcae12b2ab
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/main/java/com/baeldung/continuetransactionafterexception/model/InvoiceEntity.java
@@ -0,0 +1,62 @@
+package com.baeldung.continuetransactionafterexception.model;
+
+import java.util.Objects;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+
+@Entity
+@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "serialNumber")})
+public class InvoiceEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Integer id;
+ private String serialNumber;
+ private String description;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public void setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ InvoiceEntity that = (InvoiceEntity) o;
+ return Objects.equals(serialNumber, that.serialNumber);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(serialNumber);
+ }
+}
diff --git a/persistence-modules/spring-jpa-3/src/test/java/com/baeldung/com/baeldung/continuetransactionafterexception/ContinueWithTransactionAfterExceptionIntegrationConfiguration.java b/persistence-modules/spring-jpa-3/src/test/java/com/baeldung/com/baeldung/continuetransactionafterexception/ContinueWithTransactionAfterExceptionIntegrationConfiguration.java
new file mode 100644
index 0000000000..3a5bd1cde2
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/test/java/com/baeldung/com/baeldung/continuetransactionafterexception/ContinueWithTransactionAfterExceptionIntegrationConfiguration.java
@@ -0,0 +1,61 @@
+package com.baeldung.com.baeldung.continuetransactionafterexception;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.JpaVendorAdapter;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+
+import jakarta.persistence.EntityManagerFactory;
+
+@Configuration
+@PropertySource("continuetransactionafterexception/test.properties")
+public class ContinueWithTransactionAfterExceptionIntegrationConfiguration {
+
+ @Bean
+ public DataSource dataSource() {
+ EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
+ return dbBuilder.setType(EmbeddedDatabaseType.H2)
+ .build();
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Value("${hibernate.hbm2ddl.auto}") String hbm2ddlType, @Value("${hibernate.dialect}") String dialect, @Value("${hibernate.show_sql}") boolean showSql) {
+ LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
+
+ result.setDataSource(dataSource());
+ result.setPackagesToScan("com.baeldung.continuetransactionafterexception.model");
+ result.setJpaVendorAdapter(jpaVendorAdapter());
+
+ Map jpaProperties = new HashMap<>();
+ jpaProperties.put("hibernate.hbm2ddl.auto", hbm2ddlType);
+ jpaProperties.put("hibernate.dialect", dialect);
+ jpaProperties.put("hibernate.show_sql", showSql);
+ result.setJpaPropertyMap(jpaProperties);
+
+ return result;
+ }
+
+ @Bean
+ public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
+ JpaTransactionManager transactionManager = new JpaTransactionManager();
+ transactionManager.setEntityManagerFactory(entityManagerFactory);
+ transactionManager.setNestedTransactionAllowed(true);
+
+ return transactionManager;
+ }
+
+ public JpaVendorAdapter jpaVendorAdapter() {
+ return new HibernateJpaVendorAdapter();
+ }
+}
diff --git a/persistence-modules/spring-jpa-3/src/test/java/com/baeldung/com/baeldung/continuetransactionafterexception/ContinueWithTransactionAfterExceptionIntegrationTest.java b/persistence-modules/spring-jpa-3/src/test/java/com/baeldung/com/baeldung/continuetransactionafterexception/ContinueWithTransactionAfterExceptionIntegrationTest.java
new file mode 100644
index 0000000000..7f5f60f556
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/test/java/com/baeldung/com/baeldung/continuetransactionafterexception/ContinueWithTransactionAfterExceptionIntegrationTest.java
@@ -0,0 +1,165 @@
+package com.baeldung.com.baeldung.continuetransactionafterexception;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.transaction.UnexpectedRollbackException;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import com.baeldung.continuetransactionafterexception.InvoiceRepository;
+import com.baeldung.continuetransactionafterexception.InvoiceService;
+import com.baeldung.continuetransactionafterexception.NotificationSendingException;
+import com.baeldung.continuetransactionafterexception.model.InvoiceEntity;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {
+ ContinueWithTransactionAfterExceptionIntegrationConfiguration.class,
+ InvoiceRepository.class, InvoiceService.class})
+@DirtiesContext
+@EnableTransactionManagement
+class ContinueWithTransactionAfterExceptionIntegrationTest {
+
+ @Autowired
+ private InvoiceRepository repository;
+ @Autowired
+ private InvoiceService service;
+
+ @Test
+ void givenInvoiceService_whenExceptionOccursDuringNotificationSending_thenNoDataShouldBeSaved() {
+ InvoiceEntity invoiceEntity = new InvoiceEntity();
+ invoiceEntity.setSerialNumber("#1");
+ invoiceEntity.setDescription("First invoice");
+
+ assertThrows(
+ NotificationSendingException.class,
+ () -> service.saveInvoice(invoiceEntity)
+ );
+
+ List entityList = repository.findAll();
+ Assertions.assertTrue(entityList.isEmpty());
+ }
+
+ @Test
+ void givenInvoiceRepository_whenExceptionOccursDuringBatchSavingInternally_thenNoDataShouldBeSaved() {
+
+ List testEntities = new ArrayList<>();
+
+ InvoiceEntity invoiceEntity = new InvoiceEntity();
+ invoiceEntity.setSerialNumber("#1");
+ invoiceEntity.setDescription("First invoice");
+ testEntities.add(invoiceEntity);
+
+ InvoiceEntity invoiceEntity2 = new InvoiceEntity();
+ invoiceEntity2.setSerialNumber("#1");
+ invoiceEntity.setDescription("First invoice (duplicated)");
+ testEntities.add(invoiceEntity2);
+
+ InvoiceEntity invoiceEntity3 = new InvoiceEntity();
+ invoiceEntity3.setSerialNumber("#2");
+ invoiceEntity.setDescription("Second invoice");
+ testEntities.add(invoiceEntity3);
+
+ UnexpectedRollbackException exception = assertThrows(UnexpectedRollbackException.class,
+ () -> repository.saveBatch(testEntities));
+ assertEquals("Transaction silently rolled back because it has been marked as rollback-only",
+ exception.getMessage());
+
+ List entityList = repository.findAll();
+ Assertions.assertTrue(entityList.isEmpty());
+ }
+
+ @Test
+ void givenInvoiceService_whenNotificationSendingExceptionOccurs_thenTheInvoiceBeSaved() {
+ InvoiceEntity invoiceEntity = new InvoiceEntity();
+ invoiceEntity.setSerialNumber("#1");
+ invoiceEntity.setDescription("We want to save this invoice anyway");
+
+ assertThrows(
+ NotificationSendingException.class,
+ () -> service.saveInvoiceWithoutRollback(invoiceEntity)
+ );
+
+ List entityList = repository.findAll();
+ Assertions.assertTrue(entityList.contains(invoiceEntity));
+ }
+
+ @Test
+ void givenInvoiceRepository_whenExceptionOccursDuringBatchSavingInternally_thenDataShouldBeSavedInSeparateTransaction() {
+
+ List testEntities = new ArrayList<>();
+
+ InvoiceEntity invoiceEntity1 = new InvoiceEntity();
+ invoiceEntity1.setSerialNumber("#1");
+ invoiceEntity1.setDescription("First invoice");
+ testEntities.add(invoiceEntity1);
+
+ InvoiceEntity invoiceEntity2 = new InvoiceEntity();
+ invoiceEntity2.setSerialNumber("#1");
+ invoiceEntity1.setDescription("First invoice (duplicated)");
+ testEntities.add(invoiceEntity2);
+
+ InvoiceEntity invoiceEntity3 = new InvoiceEntity();
+ invoiceEntity3.setSerialNumber("#2");
+ invoiceEntity1.setDescription("Second invoice");
+ testEntities.add(invoiceEntity3);
+
+ repository.saveBatchUsingManualTransaction(testEntities);
+
+ List entityList = repository.findAll();
+ Assertions.assertTrue(entityList.contains(invoiceEntity1));
+ Assertions.assertTrue(entityList.contains(invoiceEntity3));
+ }
+
+ @Test
+ void givenInvoiceRepository_whenExceptionOccursDuringBatchSaving_thenDataShouldBeSavedUsingSaveMethod() {
+
+ List testEntities = new ArrayList<>();
+
+ InvoiceEntity invoiceEntity1 = new InvoiceEntity();
+ invoiceEntity1.setSerialNumber("#1");
+ invoiceEntity1.setDescription("First invoice");
+ testEntities.add(invoiceEntity1);
+
+ InvoiceEntity invoiceEntity2 = new InvoiceEntity();
+ invoiceEntity2.setSerialNumber("#1");
+ invoiceEntity1.setDescription("First invoice (duplicated)");
+ testEntities.add(invoiceEntity2);
+
+ InvoiceEntity invoiceEntity3 = new InvoiceEntity();
+ invoiceEntity3.setSerialNumber("#2");
+ invoiceEntity1.setDescription("Second invoice");
+ testEntities.add(invoiceEntity3);
+
+ try {
+ repository.saveBatchOnly(testEntities);
+ } catch (Exception e) {
+ testEntities.forEach(t -> {
+ try {
+ repository.save(t);
+ } catch (Exception e2) {
+ System.err.println(e2.getMessage());
+ }
+ });
+ }
+
+ List entityList = repository.findAll();
+ Assertions.assertTrue(entityList.contains(invoiceEntity1));
+ Assertions.assertTrue(entityList.contains(invoiceEntity3));
+ }
+
+ @AfterEach
+ void clean() {
+ repository.deleteAll();
+ }
+}
diff --git a/persistence-modules/spring-jpa-3/src/test/resources/continuetransactionafterexception/test.properties b/persistence-modules/spring-jpa-3/src/test/resources/continuetransactionafterexception/test.properties
new file mode 100644
index 0000000000..191dfc85f5
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/test/resources/continuetransactionafterexception/test.properties
@@ -0,0 +1,7 @@
+jdbc.driverClassName=org.h2.Driver
+jdbc.driverClassName=org.h2.Driver
+jdbc.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
+
+hibernate.dialect=org.hibernate.dialect.H2Dialect
+hibernate.show_sql=false
+hibernate.hbm2ddl.auto=update
diff --git a/persistence-modules/spring-jpa-3/src/test/resources/logback.xml b/persistence-modules/spring-jpa-3/src/test/resources/logback.xml
new file mode 100644
index 0000000000..4ecb8017f3
--- /dev/null
+++ b/persistence-modules/spring-jpa-3/src/test/resources/logback.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n
+
+
+
+
+
+
+