diff --git a/spring-boot-modules/spring-boot-libraries-3/README.md b/spring-boot-modules/spring-boot-libraries-3/README.md
new file mode 100644
index 0000000000..1458e3ef39
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/README.md
@@ -0,0 +1,5 @@
+## Spring Boot Libraries
+
+This module contains articles about various Spring Boot libraries
+
+### Relevant Articles:
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-libraries-3/pom.xml b/spring-boot-modules/spring-boot-libraries-3/pom.xml
new file mode 100644
index 0000000000..43d8be84a4
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/pom.xml
@@ -0,0 +1,89 @@
+
+
+ 4.0.0
+ spring-boot-libraries-3
+
+
+ spring-boot-modules
+ com.baeldung.spring-boot-modules
+ 1.0.0-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ org.springframework.modulith
+ spring-modulith-events-api
+ ${spring-modulith-events-kafka.version}
+
+
+ org.springframework.modulith
+ spring-modulith-events-kafka
+ ${spring-modulith-events-kafka.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.testcontainers
+ kafka
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+
+ com.h2database
+ h2
+ 2.2.224
+ test
+
+
+
+ org.awaitility
+ awaitility
+ ${awaitility.version}
+ test
+
+
+
+
+
+ 17
+ 3.1.5
+ 1.1.2
+ 1.19.3
+ 4.2.0
+
+
+
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/Application.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/Application.java
new file mode 100644
index 0000000000..aeaf57becd
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/Application.java
@@ -0,0 +1,13 @@
+package com.baeldung.springmodulith;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run( Application.class, args);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/Article.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/Article.java
new file mode 100644
index 0000000000..d52ed5afe5
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/Article.java
@@ -0,0 +1,45 @@
+package com.baeldung.springmodulith.events.externalization;
+
+import static jakarta.persistence.GenerationType.*;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+
+@Entity
+public class Article {
+ @Id
+ @GeneratedValue(strategy = AUTO)
+ private Long id;
+
+ private String slug;
+ private String title;
+ private String author;
+ private String content;
+
+ public Article(String slug, String title, String author, String content) {
+ this.slug = slug;
+ this.title = title;
+ this.author = author;
+ this.content = content;
+ }
+
+ public Article() {
+ }
+
+ public String author() {
+ return author;
+ }
+
+ public String content() {
+ return content;
+ }
+
+ public String slug() {
+ return slug;
+ }
+
+ public String title() {
+ return title;
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/ArticlePublishedEvent.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/ArticlePublishedEvent.java
new file mode 100644
index 0000000000..e12b6dafe5
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/ArticlePublishedEvent.java
@@ -0,0 +1,7 @@
+package com.baeldung.springmodulith.events.externalization;
+
+import org.springframework.modulith.events.Externalized;
+
+@Externalized("baeldung.article.published::#{slug()}")
+public record ArticlePublishedEvent(String slug, String title) {
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/ArticleRepository.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/ArticleRepository.java
new file mode 100644
index 0000000000..f6351b6262
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/ArticleRepository.java
@@ -0,0 +1,8 @@
+package com.baeldung.springmodulith.events.externalization;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+interface ArticleRepository extends CrudRepository {
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/Baeldung.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/Baeldung.java
new file mode 100644
index 0000000000..1d309a8653
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/Baeldung.java
@@ -0,0 +1,37 @@
+package com.baeldung.springmodulith.events.externalization;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class Baeldung {
+
+ private final ApplicationEventPublisher applicationEvents;
+ private final ArticleRepository articleRepository;
+
+
+ public Baeldung(ApplicationEventPublisher applicationEvents, ArticleRepository articleRepository) {
+ this.applicationEvents = applicationEvents;
+ this.articleRepository = articleRepository;
+ }
+
+ @Transactional
+ public void createArticle(Article article) {
+ // ... business logic
+ validateArticle(article);
+ article = addArticleTags(article);
+ article = articleRepository.save(article);
+
+ applicationEvents.publishEvent(new ArticlePublishedEvent(article.slug(), article.title()));
+ }
+
+
+ private Article addArticleTags(Article article) {
+ return article;
+ }
+
+ private void validateArticle(Article article) {
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/EventExternalizationConfig.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/EventExternalizationConfig.java
new file mode 100644
index 0000000000..9564696d92
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/EventExternalizationConfig.java
@@ -0,0 +1,41 @@
+package com.baeldung.springmodulith.events.externalization;
+
+import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.DefaultKafkaProducerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.core.ProducerFactory;
+import org.springframework.kafka.core.KafkaOperations;
+import org.springframework.modulith.events.EventExternalizationConfiguration;
+import org.springframework.modulith.events.RoutingTarget;
+
+@Configuration
+class EventExternalizationConfig {
+
+ @Bean
+ EventExternalizationConfiguration eventExternalizationConfiguration() {
+ return EventExternalizationConfiguration.externalizing()
+ .select(EventExternalizationConfiguration.annotatedAsExternalized())
+ .route(
+ ArticlePublishedEvent.class,
+ it -> RoutingTarget.forTarget("baeldung.articles.published").andKey(it.slug())
+ )
+ .mapping(
+ ArticlePublishedEvent.class,
+ it -> new ArticlePublishedKafkaEvent(it.slug(), it.title())
+ )
+ .build();
+ }
+
+ record ArticlePublishedKafkaEvent(String slug, String title) {
+ }
+
+
+ @Bean
+ KafkaOperations kafkaOperations(KafkaProperties kafkaProperties) {
+ ProducerFactory producerFactory = new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
+ return new KafkaTemplate<>(producerFactory);
+ }
+}
+
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/infra/ArticlePublishedKafkaProducer.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/infra/ArticlePublishedKafkaProducer.java
new file mode 100644
index 0000000000..17a88a73f7
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/events/externalization/infra/ArticlePublishedKafkaProducer.java
@@ -0,0 +1,34 @@
+package com.baeldung.springmodulith.events.externalization.infra;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.kafka.core.KafkaOperations;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.transaction.event.TransactionalEventListener;
+import org.springframework.util.Assert;
+
+import com.baeldung.springmodulith.events.externalization.ArticlePublishedEvent;
+
+//@Component
+// this is used in sections 3 and 4 of tha article
+// but it will cause the tests to fail if it used together with the @Externalized annotation
+class ArticlePublishedKafkaProducer {
+
+ private final KafkaOperations messageProducer;
+
+ public ArticlePublishedKafkaProducer(KafkaOperations messageProducer) {
+ this.messageProducer = messageProducer;
+ }
+
+ @EventListener
+ public void publish(ArticlePublishedEvent event) {
+ Assert.notNull(event.slug(), "Article Slug must not be null!");
+ messageProducer.send("baeldung.articles.published", event);
+ }
+
+ @Async
+ @TransactionalEventListener
+ public void publishAsync(ArticlePublishedEvent article) {
+ Assert.notNull(article.slug(), "Article Slug must not be null!");
+ messageProducer.send("baeldung.articles.published", article.slug(), article);
+ }
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/resources/application.yml b/spring-boot-modules/spring-boot-libraries-3/src/main/resources/application.yml
new file mode 100644
index 0000000000..ad8f7ab4e6
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/resources/application.yml
@@ -0,0 +1,12 @@
+logging.level.org.springframework.orm.jpa: TRACE
+
+spring.kafka:
+ bootstrap-servers: localhost:9092
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+ type-mapping: com.baeldung.annotation.events.externalization.producer.ArticlePublished
+ consumer:
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ auto-offset-reset: earliest
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/EventsExternalizationLiveTest.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/EventsExternalizationLiveTest.java
new file mode 100644
index 0000000000..7f4f7a8224
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/EventsExternalizationLiveTest.java
@@ -0,0 +1,89 @@
+package com.baeldung.springmodulith.events.externalization;
+
+import static java.time.Duration.ofMillis;
+import static java.time.Duration.ofSeconds;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.KafkaContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.shaded.org.awaitility.Awaitility;
+import org.testcontainers.utility.DockerImageName;
+
+import com.baeldung.springmodulith.Application;
+import com.baeldung.springmodulith.events.externalization.listener.TestKafkaListenerConfig;
+import com.baeldung.springmodulith.events.externalization.listener.TestListener;
+
+@Testcontainers
+@SpringBootTest(classes = { Application.class, TestKafkaListenerConfig.class })
+class EventsExternalizationLiveTest {
+
+ @Autowired
+ private Baeldung baeldung;
+ @Autowired
+ private TestListener listener;
+ @Autowired
+ private ArticleRepository repository;
+
+ @Container
+ static KafkaContainer kafkaContainer = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"));
+
+ @DynamicPropertySource
+ static void dynamicProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.kafka.bootstrap-servers", kafkaContainer::getBootstrapServers);
+ }
+
+ static {
+ Awaitility.setDefaultTimeout(ofSeconds(3));
+ Awaitility.setDefaultPollDelay(ofMillis(100));
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ listener.reset();
+ repository.deleteAll();
+ }
+
+ @Test
+ void whenArticleIsSavedToDB_thenItIsAlsoPublishedToKafka() {
+ var article = new Article("introduction-to-spring-boot", "Introduction to Spring Boot", "John Doe", " Spring Boot is [...]
");
+
+ baeldung.createArticle(article);
+
+ await().untilAsserted(() ->
+ assertThat(listener.getEvents())
+ .hasSize(1)
+ .first().asString()
+ .contains("\"slug\":\"introduction-to-spring-boot\"")
+ .contains("\"title\":\"Introduction to Spring Boot\""));
+
+ assertThat(repository.findAll())
+ .hasSize(1)
+ .first()
+ .extracting(Article::slug, Article::title)
+ .containsExactly("introduction-to-spring-boot", "Introduction to Spring Boot");
+ }
+
+ @Test
+ void whenPublishingMessageFails_thenArticleIsStillSavedToDB() {
+ var article = new Article(null, "Introduction to Spring Boot", "John Doe", " Spring Boot is [...]
");
+
+ baeldung.createArticle(article);
+
+ assertThat(listener.getEvents())
+ .isEmpty();
+
+ assertThat(repository.findAll())
+ .hasSize(1)
+ .first()
+ .extracting(Article::title, Article::author)
+ .containsExactly("Introduction to Spring Boot", "John Doe");
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/listener/TestKafkaListenerConfig.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/listener/TestKafkaListenerConfig.java
new file mode 100644
index 0000000000..c2ee9b24a2
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/listener/TestKafkaListenerConfig.java
@@ -0,0 +1,31 @@
+package com.baeldung.springmodulith.events.externalization.listener;
+
+import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.config.KafkaListenerContainerFactory;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
+
+@EnableKafka
+@Configuration
+public class TestKafkaListenerConfig {
+
+ @Bean
+ KafkaListenerContainerFactory> kafkaListenerContainerFactory(
+ ConsumerFactory consumerFactory
+ ) {
+ var factory = new ConcurrentKafkaListenerContainerFactory();
+ factory.setConsumerFactory(consumerFactory);
+ return factory;
+ }
+
+ @Bean
+ ConsumerFactory consumerFactory(KafkaProperties kafkaProperties) {
+ return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/listener/TestListener.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/listener/TestListener.java
new file mode 100644
index 0000000000..bf5a36f66f
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/events/externalization/listener/TestListener.java
@@ -0,0 +1,25 @@
+package com.baeldung.springmodulith.events.externalization.listener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TestListener {
+ private List events = new ArrayList<>();
+
+ @KafkaListener(id = "test-id", topics = "baeldung.articles.published")
+ public void listen(String event) {
+ events.add(event);
+ }
+
+ public List getEvents() {
+ return events;
+ }
+
+ public void reset() {
+ events = new ArrayList<>();
+ }
+}
\ No newline at end of file