diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index c4893df759..8691ce1f09 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -41,6 +41,26 @@
guava
21.0
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+ org.junit.platform
+ junit-platform-launcher
+ ${junit-platform.version}
+ test
+
\ No newline at end of file
diff --git a/spring-data-jpa/src/main/java/com/baeldung/Application.java b/spring-data-jpa/src/main/java/com/baeldung/Application.java
index 4e14f94311..72d29d9fa5 100644
--- a/spring-data-jpa/src/main/java/com/baeldung/Application.java
+++ b/spring-data-jpa/src/main/java/com/baeldung/Application.java
@@ -1,11 +1,11 @@
package com.baeldung;
-import com.baeldung.dao.repositories.impl.ExtendedRepositoryImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import com.baeldung.dao.repositories.impl.ExtendedRepositoryImpl;
+
@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = ExtendedRepositoryImpl.class)
public class Application {
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate.java
new file mode 100644
index 0000000000..bf6ff0a0b9
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate.java
@@ -0,0 +1,46 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Transient;
+
+import org.springframework.context.ApplicationEventPublisher;
+
+@Entity
+class Aggregate {
+ @Transient
+ private ApplicationEventPublisher eventPublisher;
+ @Id
+ private long id;
+
+ private Aggregate() {
+ }
+
+ Aggregate(long id, ApplicationEventPublisher eventPublisher) {
+ this.id = id;
+ this.eventPublisher = eventPublisher;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "DomainEntity [id=" + id + "]";
+ }
+
+ void domainOperation() {
+ // some business logic
+ if (eventPublisher != null) {
+ eventPublisher.publishEvent(new DomainEvent());
+ }
+ }
+
+ long getId() {
+ return id;
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate2.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate2.java
new file mode 100644
index 0000000000..3d2816299a
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate2.java
@@ -0,0 +1,44 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Transient;
+
+import org.springframework.data.domain.AfterDomainEventPublication;
+import org.springframework.data.domain.DomainEvents;
+
+@Entity
+public class Aggregate2 {
+ @Transient
+ private final Collection domainEvents;
+ @Id
+ @GeneratedValue
+ private long id;
+
+ public Aggregate2() {
+ domainEvents = new ArrayList<>();
+ }
+
+ @AfterDomainEventPublication
+ public void clearEvents() {
+ domainEvents.clear();
+ }
+
+ public void domainOperation() {
+ // some domain operation
+ domainEvents.add(new DomainEvent());
+ }
+
+ @DomainEvents
+ public Collection events() {
+ return domainEvents;
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate2Repository.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate2Repository.java
new file mode 100644
index 0000000000..2a95abe347
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate2Repository.java
@@ -0,0 +1,10 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface Aggregate2Repository extends CrudRepository {
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate3.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate3.java
new file mode 100644
index 0000000000..e0c3131b06
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate3.java
@@ -0,0 +1,23 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+import org.springframework.data.domain.AbstractAggregateRoot;
+
+@Entity
+public class Aggregate3 extends AbstractAggregateRoot {
+ @Id
+ @GeneratedValue
+ private long id;
+
+ public void domainOperation() {
+ // some domain operation
+ registerEvent(new DomainEvent());
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate3Repository.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate3Repository.java
new file mode 100644
index 0000000000..e442bdb210
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/Aggregate3Repository.java
@@ -0,0 +1,14 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * @author goobar
+ *
+ */
+public interface Aggregate3Repository extends CrudRepository {
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/AggregateRepository.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/AggregateRepository.java
new file mode 100644
index 0000000000..5a15156d03
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/AggregateRepository.java
@@ -0,0 +1,10 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface AggregateRepository extends CrudRepository {
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DddConfig.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DddConfig.java
new file mode 100644
index 0000000000..1315b11875
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DddConfig.java
@@ -0,0 +1,15 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@PropertySource("classpath:/ddd.properties")
+public class DddConfig {
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DomainEvent.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DomainEvent.java
new file mode 100644
index 0000000000..1e6479d4fc
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DomainEvent.java
@@ -0,0 +1,8 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+class DomainEvent {
+
+}
diff --git a/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DomainService.java b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DomainService.java
new file mode 100644
index 0000000000..082c9bd88e
--- /dev/null
+++ b/spring-data-jpa/src/main/java/com/baeldung/ddd/event/DomainService.java
@@ -0,0 +1,31 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import javax.transaction.Transactional;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DomainService {
+ private final ApplicationEventPublisher eventPublisher;
+ private final AggregateRepository repository;
+
+ public DomainService(AggregateRepository repository, ApplicationEventPublisher eventPublisher) {
+ this.repository = repository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Transactional
+ public void serviceDomainOperation(long entityId) {
+ repository.findById(entityId)
+ .ifPresent(entity -> {
+ entity.domainOperation();
+ repository.save(entity);
+ eventPublisher.publishEvent(new DomainEvent());
+ });
+ }
+
+}
diff --git a/spring-data-jpa/src/main/resources/ddd.properties b/spring-data-jpa/src/main/resources/ddd.properties
new file mode 100644
index 0000000000..e5126b694b
--- /dev/null
+++ b/spring-data-jpa/src/main/resources/ddd.properties
@@ -0,0 +1 @@
+spring.datasource.initialization-mode=never
\ No newline at end of file
diff --git a/spring-data-jpa/src/main/resources/persistence-multiple-db.properties b/spring-data-jpa/src/main/resources/persistence-multiple-db.properties
index 53c6cecf8b..75534e8a54 100644
--- a/spring-data-jpa/src/main/resources/persistence-multiple-db.properties
+++ b/spring-data-jpa/src/main/resources/persistence-multiple-db.properties
@@ -3,7 +3,7 @@ jdbc.driverClassName=org.h2.Driver
user.jdbc.url=jdbc:h2:mem:spring_jpa_user;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS USERS
product.jdbc.url=jdbc:h2:mem:spring_jpa_product;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS PRODUCTS
jdbc.user=sa
-jdbc.pass=
+jdbc.pass=sa
# hibernate.X
hibernate.dialect=org.hibernate.dialect.H2Dialect
diff --git a/spring-data-jpa/src/main/resources/persistence.properties b/spring-data-jpa/src/main/resources/persistence.properties
index 5e83653401..3543e1b52b 100644
--- a/spring-data-jpa/src/main/resources/persistence.properties
+++ b/spring-data-jpa/src/main/resources/persistence.properties
@@ -2,7 +2,7 @@
jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS USERS
jdbc.user=sa
-jdbc.pass=
+jdbc.pass=sa
# hibernate.X
hibernate.dialect=org.hibernate.dialect.H2Dialect
diff --git a/spring-data-jpa/src/test/java/com/baeldung/ddd/event/Aggregate2EventsIntegrationTest.java b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/Aggregate2EventsIntegrationTest.java
new file mode 100644
index 0000000000..3f650d4d63
--- /dev/null
+++ b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/Aggregate2EventsIntegrationTest.java
@@ -0,0 +1,68 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+@SpringJUnitConfig
+@SpringBootTest
+class Aggregate2EventsIntegrationTest {
+ @MockBean
+ private TestEventHandler eventHandler;
+ @Autowired
+ private Aggregate2Repository repository;
+
+ // @formatter:off
+ @DisplayName("given aggregate with @AfterDomainEventPublication,"
+ + " when do domain operation and save twice,"
+ + " then an event is published only for the first time")
+ // @formatter:on
+ @Test
+ void afterDomainEvents() {
+ // given
+ Aggregate2 aggregate = new Aggregate2();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ repository.deleteAll();
+ }
+
+ // @formatter:off
+ @DisplayName("given aggregate with @DomainEvents,"
+ + " when do domain operation and save,"
+ + " then an event is published")
+ // @formatter:on
+ @Test
+ void domainEvents() {
+ // given
+ Aggregate2 aggregate = new Aggregate2();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+}
diff --git a/spring-data-jpa/src/test/java/com/baeldung/ddd/event/Aggregate3EventsIntegrationTest.java b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/Aggregate3EventsIntegrationTest.java
new file mode 100644
index 0000000000..893dcac3f8
--- /dev/null
+++ b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/Aggregate3EventsIntegrationTest.java
@@ -0,0 +1,63 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+@SpringJUnitConfig
+@SpringBootTest
+class Aggregate3EventsIntegrationTest {
+
+ @MockBean
+ private TestEventHandler eventHandler;
+ @Autowired
+ private Aggregate3Repository repository;
+
+ // @formatter:off
+ @DisplayName("given aggregate extending AbstractAggregateRoot,"
+ + " when do domain operation and save twice,"
+ + " then an event is published only for the first time")
+ // @formatter:on
+ @Test
+ void afterDomainEvents() {
+ // given
+ Aggregate3 aggregate = new Aggregate3();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+ // @formatter:off
+ @DisplayName("given aggregate extending AbstractAggregateRoot,"
+ + " when do domain operation and save,"
+ + " then an event is published")
+ // @formatter:on
+ @Test
+ void domainEvents() {
+ // given
+ Aggregate3 aggregate = new Aggregate3();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+}
diff --git a/spring-data-jpa/src/test/java/com/baeldung/ddd/event/AggregateEventsIntegrationTest.java b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/AggregateEventsIntegrationTest.java
new file mode 100644
index 0000000000..f0e1147245
--- /dev/null
+++ b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/AggregateEventsIntegrationTest.java
@@ -0,0 +1,82 @@
+package com.baeldung.ddd.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+@SpringJUnitConfig
+@SpringBootTest
+class AggregateEventsIntegrationTest {
+
+ @Autowired
+ private DomainService domainService;
+
+ @MockBean
+ private TestEventHandler eventHandler;
+ @Autowired
+ private ApplicationEventPublisher eventPublisher;
+ @Autowired
+ private AggregateRepository repository;
+
+ // @formatter:off
+ @DisplayName("given existing aggregate,"
+ + " when do domain operation directly on aggregate,"
+ + " then domain event is NOT published")
+ // @formatter:on
+ @Test
+ void aggregateEventsTest() {
+ Aggregate existingDomainEntity = new Aggregate(0, eventPublisher);
+ repository.save(existingDomainEntity);
+
+ // when
+ repository.findById(existingDomainEntity.getId())
+ .get()
+ .domainOperation();
+
+ // then
+ verifyZeroInteractions(eventHandler);
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ repository.deleteAll();
+ }
+
+ // @formatter:off
+ @DisplayName("given existing aggregate,"
+ + " when do domain operation on service,"
+ + " then domain event is published")
+ // @formatter:on
+ @Test
+ void serviceEventsTest() {
+ Aggregate existingDomainEntity = new Aggregate(1, eventPublisher);
+ repository.save(existingDomainEntity);
+
+ // when
+ domainService.serviceDomainOperation(existingDomainEntity.getId());
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+ @TestConfiguration
+ public static class TestConfig {
+ @Bean
+ public DomainService domainService(AggregateRepository repository, ApplicationEventPublisher eventPublisher) {
+ return new DomainService(repository, eventPublisher);
+ }
+ }
+
+}
diff --git a/spring-data-jpa/src/test/java/com/baeldung/ddd/event/TestEventHandler.java b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/TestEventHandler.java
new file mode 100644
index 0000000000..721402c17a
--- /dev/null
+++ b/spring-data-jpa/src/test/java/com/baeldung/ddd/event/TestEventHandler.java
@@ -0,0 +1,12 @@
+/**
+ *
+ */
+package com.baeldung.ddd.event;
+
+import org.springframework.transaction.event.TransactionalEventListener;
+
+interface TestEventHandler {
+ @TransactionalEventListener
+ void handleEvent(DomainEvent event);
+
+}