diff --git a/.github/scripts/update_sdk_version.sh b/.github/scripts/update_sdk_version.sh
index fba6ff9af..f66d2adf2 100755
--- a/.github/scripts/update_sdk_version.sh
+++ b/.github/scripts/update_sdk_version.sh
@@ -27,4 +27,7 @@ mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f testcontainers-dap
# dapr-spring
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f dapr-spring/pom.xml
+# spring-boot-examples
+mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f spring-boot-examples/pom.xml
+
git clean -f
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 8d1e23187..4e606d00f 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -164,3 +164,7 @@ jobs:
working-directory: ./examples
run: |
mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md
+ - name: Validate Spring Boot examples
+ working-directory: ./spring-boot-examples
+ run: |
+ mm.py README.md
diff --git a/README.md b/README.md
index 780c66412..f962cbc08 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ This is the Dapr SDK for Java, including the following features:
* Binding
* State Store
* Actors
+* Workflows
## Getting Started
@@ -112,6 +113,13 @@ Try the following examples to learn more about Dapr's Java SDK:
* [Exception handling](./examples/src/main/java/io/dapr/examples/exception)
* [Unit testing](./examples/src/main/java/io/dapr/examples/unittesting)
+### Running Spring Boot examples
+
+The Spring Boot integration for Dapr use [Testcontainers](https://testcontainers.com) to set up a local environment development flow that doesn't
+require the use of the `dapr` CLI and it integrates with the Spring Boot programming model.
+
+You can find a [step-by-step tutorial showing this integration here](./spring-boot-examples/README.md).
+
### API Documentation
Please, refer to our [Javadoc](https://dapr.github.io/java-sdk/) website.
diff --git a/pom.xml b/pom.xml
index 1448ac448..53b2908e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -337,6 +337,7 @@
sdk-springboot
dapr-spring
examples
+ spring-boot-examples
testcontainers-dapr
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java
index ed7c14ee5..b617bfdf6 100644
--- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerIT.java
@@ -63,9 +63,10 @@ public class DaprContainerIT {
@Container
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG)
- .withAppName("dapr-app")
- .withAppPort(8081)
- .withAppChannelAddress("host.testcontainers.internal");
+ .withAppName("dapr-app")
+ .withAppPort(8081)
+ .withAppHealthCheckPath("/actuator/health")
+ .withAppChannelAddress("host.testcontainers.internal");
/**
* Sets the Dapr properties for the test.
@@ -77,18 +78,21 @@ public class DaprContainerIT {
}
private void configStub() {
+ stubFor(any(urlMatching("/actuator/health"))
+ .willReturn(aResponse().withBody("[]").withStatus(200)));
+
stubFor(any(urlMatching("/dapr/subscribe"))
- .willReturn(aResponse().withBody("[]").withStatus(200)));
+ .willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(get(urlMatching("/dapr/config"))
- .willReturn(aResponse().withBody("[]").withStatus(200)));
+ .willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(any(urlMatching("/([a-z1-9]*)"))
- .willReturn(aResponse().withBody("[]").withStatus(200)));
+ .willReturn(aResponse().withBody("[]").withStatus(200)));
// create a stub
stubFor(post(urlEqualTo("/events"))
- .willReturn(aResponse().withBody("event received!").withStatus(200)));
+ .willReturn(aResponse().withBody("event received!").withStatus(200)));
configureFor("localhost", 8081);
}
@@ -96,13 +100,13 @@ public class DaprContainerIT {
@Test
public void testDaprContainerDefaults() {
assertEquals(2,
- DAPR_CONTAINER.getComponents().size(),
- "The pubsub and kvstore component should be configured by default"
+ DAPR_CONTAINER.getComponents().size(),
+ "The pubsub and kvstore component should be configured by default"
);
assertEquals(
- 1,
- DAPR_CONTAINER.getSubscriptions().size(),
- "A subscription should be configured by default if none is provided"
+ 1,
+ DAPR_CONTAINER.getSubscriptions().size(),
+ "A subscription should be configured by default if none is provided"
);
}
@@ -129,10 +133,10 @@ public class DaprContainerIT {
Thread.sleep(1000);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
- .build();
+ .build();
Request request = new Request.Builder()
- .url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
- .build();
+ .url(DAPR_CONTAINER.getHttpEndpoint() + "/v1.0/metadata")
+ .build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
@@ -158,7 +162,7 @@ public class DaprContainerIT {
private DaprClientBuilder createDaprClientBuilder() {
return new DaprClientBuilder()
- .withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
- .withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
+ .withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
+ .withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint());
}
}
diff --git a/spring-boot-examples/README.md b/spring-boot-examples/README.md
new file mode 100644
index 000000000..3cc88610d
--- /dev/null
+++ b/spring-boot-examples/README.md
@@ -0,0 +1,176 @@
+# Dapr Spring Boot and Testcontainers integration Example
+
+This example consists of two applications:
+- Producer App:
+ - Publish messages using a Spring Messaging approach
+ - Store and retrieve information using Spring Data CrudRepository
+ - Implements a Workflow with Dapr Workflows
+- Consumer App:
+ - Subscribe to messages
+
+## Running these examples from source code
+
+To run these examples you will need:
+- Java SDK
+- Maven
+- Docker or a container runtime such as Podman
+
+From the `spring-boot-examples/` directory you can start each service using the test configuration that uses
+[Testcontainers](https://testcontainers.com) to boostrap [Dapr](https://dapr.io) by running the following command:
+
+
+
+
+```sh
+cd producer-app/
+../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run
+```
+
+
+
+This will start the `producer-app` with Dapr services and the infrastructure needed by the application to run,
+in this case RabbitMQ and PostgreSQL. The `producer-app` starts on port `8080` by default.
+
+The `-Dspring-boot.run.arguments="--reuse=true"` flag helps the application to connect to an existing shared
+infrastructure if it already exists. For development purposes, and to connect both applications we will set the flag
+in both. For more details check the `DaprTestContainersConfig.java` classes in both, the `producer-app` and the `consumer-app`.
+
+Then run in a different terminal:
+
+
+
+
+```sh
+cd consumer-app/
+../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run
+```
+
+
+The `consumer-app` starts in port `8081` by default.
+
+## Interacting with the applications
+
+Now that both applications are up you can place an order by sending a POST request to `:8080/orders/`
+You can use `curl` to send a POST request to the `producer-app`:
+
+
+
+
+
+```sh
+curl -X POST localhost:8080/orders -H 'Content-Type: application/json' -d '{ "item": "the mars volta EP", "amount": 1 }'
+```
+
+
+
+
+If you check the `producer-app` logs you should see the following lines:
+
+```bash
+...
+Storing Order: Order{id='null', item='the mars volta EP', amount=1}
+Publishing Order Event: Order{id='d4f8ea15-b774-441e-bcd2-7a4208a80bec', item='the mars volta EP', amount=1}
+
+```
+
+If you check the `consumer-app` logs you should see the following lines, showing that the message
+published by the `producer-app` was correctly consumed by the `consumer-app`:
+
+```bash
+Order Event Received: Order{id='d4f8ea15-b774-441e-bcd2-7a4208a80bec', item='the mars volta EP', amount=1}
+```
+
+Next, you can create a new customer to trigger the customer's tracking workflow:
+
+
+
+
+```sh
+curl -X POST localhost:8080/customers -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
+```
+
+
+
+
+A new Workflow Instance was created to track the customers interactions. Now, the workflow instance
+is waiting for the customer to request a follow-up.
+
+You should see in the `producer-app` logs:
+
+```bash
+Workflow instance started
+Let's register the customer: salaboy
+Customer: salaboy registered.
+Let's wait for the customer: salaboy to request a follow up.
+```
+
+Send an event simulating the customer request for a follow-up:
+
+
+
+
+```sh
+curl -X POST localhost:8080/customers/followup -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
+```
+
+
+
+In the `producer-app` logs you should see that the workflow instance id moved forward to the Customer Follow Up activity:
+
+```bash
+Customer follow-up requested: salaboy
+Let's book a follow up for the customer: salaboy
+Customer: salaboy follow-up done.
+Congratulations the customer: salaboy is happy!
+```
+
+## Running on Kubernetes
+
+You can run the same example on a Kubernetes cluster. [Check the Kubernetes tutorial here](kubernetes/README.md).
diff --git a/spring-boot-examples/consumer-app/pom.xml b/spring-boot-examples/consumer-app/pom.xml
new file mode 100644
index 000000000..ebbffe52a
--- /dev/null
+++ b/spring-boot-examples/consumer-app/pom.xml
@@ -0,0 +1,99 @@
+
+
+ 4.0.0
+
+ io.dapr
+ spring-boot-examples
+ 0.14.0-SNAPSHOT
+
+
+ consumer-app
+ consumer-app
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Consumer App
+
+
+ 3.2.6
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${springboot.version}
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+ ${dapr-java-sdk.alpha-version}
+
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+ ${dapr.sdk.alpha.version}
+
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ ${dapr.sdk.alpha.version}
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ postgresql
+ 1.20.0
+ test
+
+
+ org.testcontainers
+ rabbitmq
+ 1.20.0
+ test
+
+
+ org.testcontainers
+ kafka
+ 1.20.0
+ test
+
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java
new file mode 100644
index 000000000..fbf4e005a
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/ConsumerApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ConsumerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ConsumerApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java
new file mode 100644
index 000000000..80fc2d0f2
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/Order.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+public class Order {
+ private String id;
+ private String item;
+ private Integer amount;
+
+ public Order() {
+ }
+
+ /**
+ * Creates a new Order.
+ *
+ * @param id order id
+ * @param item item reference
+ * @param amount of items in the order
+ */
+ public Order(String id, String item, Integer amount) {
+ this.id = id;
+ this.item = item;
+ this.amount = amount;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getItem() {
+ return item;
+ }
+
+ public void setItem(String item) {
+ this.item = item;
+ }
+
+ public Integer getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Integer amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public String toString() {
+ return "Order{" + "id='" + id + '\'' + ", item='" + item + '\'' + ", amount=" + amount + '}';
+ }
+}
diff --git a/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java
new file mode 100644
index 000000000..42617699f
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/main/java/io/dapr/springboot/examples/consumer/SubscriberRestController.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+import io.dapr.Topic;
+import io.dapr.client.domain.CloudEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+public class SubscriberRestController {
+
+ private final Logger logger = LoggerFactory.getLogger(SubscriberRestController.class);
+
+ private List events = new ArrayList<>();
+
+ /**
+ * Subscribe to cloud events.
+ * @param cloudEvent payload
+ */
+ @PostMapping("subscribe")
+ @Topic(pubsubName = "pubsub", name = "topic")
+ public void subscribe(@RequestBody CloudEvent cloudEvent) {
+ logger.info("Order Event Received: " + cloudEvent.getData());
+ events.add(cloudEvent);
+ }
+
+ @GetMapping("events")
+ public List getAllEvents() {
+ return events;
+ }
+
+}
+
diff --git a/spring-boot-examples/consumer-app/src/main/resources/application.properties b/spring-boot-examples/consumer-app/src/main/resources/application.properties
new file mode 100644
index 000000000..b01c2106d
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+dapr.pubsub.name=pubsub
+spring.application.name=consumer-app
+server.port=8081
+
diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java
new file mode 100644
index 000000000..6a5f8a84d
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTestConfiguration.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+import io.dapr.client.DaprClient;
+import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties;
+import io.dapr.spring.messaging.DaprMessagingTemplate;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties({DaprPubSubProperties.class})
+public class ConsumerAppTestConfiguration {
+ @Bean
+ public DaprMessagingTemplate messagingTemplate(DaprClient daprClient,
+ DaprPubSubProperties daprPubSubProperties) {
+ return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
+ }
+}
diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java
new file mode 100644
index 000000000..e7e6f0c03
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+import io.dapr.client.DaprClient;
+import io.dapr.spring.messaging.DaprMessagingTemplate;
+import io.dapr.springboot.DaprAutoConfiguration;
+import io.dapr.testcontainers.DaprContainer;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.BeforeAll;
+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.testcontainers.containers.wait.strategy.Wait;
+
+import java.io.IOException;
+import java.time.Duration;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+
+@SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class,
+ ConsumerAppTestConfiguration.class, DaprAutoConfiguration.class},
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+class ConsumerAppTests {
+
+ private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
+
+ @Autowired
+ private DaprMessagingTemplate messagingTemplate;
+
+ @Autowired
+ private SubscriberRestController subscriberRestController;
+
+ @Autowired
+ private DaprClient daprClient;
+
+ @Autowired
+ private DaprContainer daprContainer;
+
+ @BeforeAll
+ public static void setup() {
+ org.testcontainers.Testcontainers.exposeHostPorts(8081);
+ }
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + 8081;
+ Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(daprContainer);
+ }
+
+
+ @Test
+ void testMessageConsumer() throws InterruptedException, IOException {
+
+ messagingTemplate.send("topic", new Order("abc-123", "the mars volta LP", 1));
+
+ given().contentType(ContentType.JSON)
+ .when()
+ .get("/events")
+ .then()
+ .statusCode(200);
+
+ await().atMost(Duration.ofSeconds(10))
+ .until(subscriberRestController.getAllEvents()::size, equalTo(1));
+
+ }
+
+}
diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java
new file mode 100644
index 000000000..7d2218992
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+import io.dapr.testcontainers.Component;
+import io.dapr.testcontainers.DaprContainer;
+import io.dapr.testcontainers.DaprLogLevel;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.RabbitMQContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@TestConfiguration(proxyBeanMethods = false)
+public class DaprTestContainersConfig {
+
+ @Bean
+ public Network getDaprNetwork() {
+ Network defaultDaprNetwork = new Network() {
+ @Override
+ public String getId() {
+ return "dapr-network";
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return null;
+ }
+ };
+
+ List networks = DockerClientFactory.instance().client().listNetworksCmd()
+ .withNameFilter("dapr-network").exec();
+ if (networks.isEmpty()) {
+ Network.builder().createNetworkCmdModifier(cmd -> cmd.withName("dapr-network")).build().getId();
+ return defaultDaprNetwork;
+ } else {
+ return defaultDaprNetwork;
+ }
+ }
+
+ @Bean
+ public RabbitMQContainer rabbitMQContainer(Network daprNetwork, Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine"))
+ .withExposedPorts(5672)
+ .withNetworkAliases("rabbitmq")
+ .withReuse(true)
+ .withNetwork(daprNetwork);
+ }
+
+ @Bean
+ @ServiceConnection
+ public DaprContainer daprContainer(Network daprNetwork, RabbitMQContainer rabbitMQContainer, Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ Map rabbitMqProperties = new HashMap<>();
+ rabbitMqProperties.put("connectionString", "amqp://guest:guest@rabbitmq:5672");
+ rabbitMqProperties.put("user", "guest");
+ rabbitMqProperties.put("password", "guest");
+
+ return new DaprContainer("daprio/daprd:1.14.4")
+ .withAppName("consumer-app")
+ .withNetwork(daprNetwork).withComponent(new Component("pubsub",
+ "pubsub.rabbitmq", "v1", rabbitMqProperties))
+ .withDaprLogLevel(DaprLogLevel.INFO)
+ .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
+ .withAppPort(8081).withAppChannelAddress("host.testcontainers.internal")
+ .withReusablePlacement(reuse)
+ .withAppHealthCheckPath("/actuator/health")
+ .dependsOn(rabbitMQContainer);
+ }
+
+
+}
diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java
new file mode 100644
index 000000000..d37150746
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/TestConsumerApplication.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.consumer;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class TestConsumerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.from(ConsumerApplication::main)
+ .with(DaprTestContainersConfig.class)
+ .run(args);
+ org.testcontainers.Testcontainers.exposeHostPorts(8081);
+ }
+
+
+}
diff --git a/spring-boot-examples/consumer-app/src/test/resources/application.properties b/spring-boot-examples/consumer-app/src/test/resources/application.properties
new file mode 100644
index 000000000..d6cb8d293
--- /dev/null
+++ b/spring-boot-examples/consumer-app/src/test/resources/application.properties
@@ -0,0 +1,2 @@
+dapr.pubsub.name=pubsub
+server.port=8081
diff --git a/spring-boot-examples/kubernetes/README.md b/spring-boot-examples/kubernetes/README.md
new file mode 100644
index 000000000..4c3fc709d
--- /dev/null
+++ b/spring-boot-examples/kubernetes/README.md
@@ -0,0 +1,100 @@
+# Running this example on Kubernetes
+
+To run this example on Kubernetes, you can use any Kubernetes distribution.
+We install Dapr on a Kubernetes cluster and then we will deploy both the `producer-app` and `consumer-app`.
+
+## Creating a cluster and installing Dapr
+
+If you don't have any Kubernetes cluster you can use Kubernetes KIND to create a local cluster. We will create a cluster
+with a local container registry, so we can push our container images to it. This is covered in the
+[KIND documentation here](https://kind.sigs.k8s.io/docs/user/local-registry/).
+
+```bash
+./kind-with-registry.sh
+```
+
+Once you have the cluster up and running you can install Dapr:
+
+```bash
+helm repo add dapr https://dapr.github.io/helm-charts/
+helm repo update
+helm upgrade --install dapr dapr/dapr \
+--version=1.14.4 \
+--namespace dapr-system \
+--create-namespace \
+--wait
+```
+
+## Creating containers using Spring Boot and pushing to local registry
+
+Now that we have our cluster set up with a local container registry, we need to build our `producer-app` and `consumer-app` containers.
+For this we will use Spring Boot build it functions to create container images using [Buildpacks](https://buildpacks.io):
+
+From inside the `spring-boot-examples/producer-app` directory you can run the following command to create a container:
+```bash
+mvn spring-boot:build-image
+```
+
+Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster.
+Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly.
+
+```bash
+docker tag producer-app:0.14.0-SNAPSHOT localhost:5001/sb-producer-app
+docker push localhost:5001/sb-producer-app
+```
+
+From inside the `spring-boot-examples/consumer-app` directory you can run the following command to create a container:
+```bash
+mvn spring-boot:build-image
+```
+
+Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster.
+Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly.
+
+```bash
+docker tag consumer-app:0.14.0-SNAPSHOT localhost:5001/sb-consumer-app
+docker push localhost:5001/sb-consumer-app
+```
+
+Now we are ready to install our application into the cluster.
+
+## Installing and interacting with the application
+
+Now that we have a running Kubernetes cluster, we need to first install the components needed by the application.
+In this case RabbitMQ and PostgreSQL. We will use Helm to do so:
+
+Let's start with RabbitMQ:
+```bash
+helm install rabbitmq oci://registry-1.docker.io/bitnamicharts/rabbitmq --set auth.username=guest --set auth.password=guest --set auth.erlangCookie=ABC
+```
+
+Then PostgreSQL:
+```bash
+helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql --set global.postgresql.auth.database=dapr --set global.postgresql.auth.postgresPassword=password
+```
+
+Once we have these components up and running we can install the application by running from inside
+the `spring-boot-examples/kubernetes/` directory:
+
+```bash
+kubectl apply -f .
+```
+
+Next you need to use `kubectl port-forward` to be able to send requests to the applications.
+
+```bash
+kubectl port-forward svc/producer-app 8080:8080
+```
+
+In a different terminals you can check the logs of the `producer-app` and `consumer-app`:
+
+```bash
+kubectl logs -f producer-app-
+```
+and
+
+```bash
+kubectl logs -f consumer-app-
+```
+
+
diff --git a/spring-boot-examples/kubernetes/consumer-app.yaml b/spring-boot-examples/kubernetes/consumer-app.yaml
new file mode 100644
index 000000000..ab9041783
--- /dev/null
+++ b/spring-boot-examples/kubernetes/consumer-app.yaml
@@ -0,0 +1,45 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: consumer-app
+ name: consumer-app
+spec:
+ type: NodePort
+ ports:
+ - name: "consumer-app"
+ port: 8081
+ targetPort: 8081
+ nodePort: 31001
+ selector:
+ app: consumer-app
+
+---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app: consumer-app
+ name: consumer-app
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: consumer-app
+ template:
+ metadata:
+ annotations:
+ dapr.io/app-id: consumer-app
+ dapr.io/app-port: "8081"
+ dapr.io/enabled: "true"
+ labels:
+ app: consumer-app
+ spec:
+ containers:
+ - image: localhost:5001/sb-consumer-app
+ name: consumer-app
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 8081
+ name: consumer-app
diff --git a/spring-boot-examples/kubernetes/kind-with-registry.sh b/spring-boot-examples/kubernetes/kind-with-registry.sh
new file mode 100755
index 000000000..9fe55a821
--- /dev/null
+++ b/spring-boot-examples/kubernetes/kind-with-registry.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+set -o errexit
+
+# 1. Create registry container unless it already exists
+reg_name='kind-registry'
+reg_port='5001'
+if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
+ docker run \
+ -d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
+ registry:2
+fi
+
+# 2. Create kind cluster with containerd registry config dir enabled
+# TODO: kind will eventually enable this by default and this patch will
+# be unnecessary.
+#
+# See:
+# https://github.com/kubernetes-sigs/kind/issues/2875
+# https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
+# See: https://github.com/containerd/containerd/blob/main/docs/hosts.md
+cat <
+
+ 4.0.0
+
+ io.dapr
+ dapr-sdk-parent
+ 1.14.0-SNAPSHOT
+
+
+ spring-boot-examples
+ 0.14.0-SNAPSHOT
+ pom
+
+
+ producer-app
+ consumer-app
+
+
+
diff --git a/spring-boot-examples/producer-app/pom.xml b/spring-boot-examples/producer-app/pom.xml
new file mode 100644
index 000000000..c9c6a7e76
--- /dev/null
+++ b/spring-boot-examples/producer-app/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ spring-boot-examples
+ 0.14.0-SNAPSHOT
+
+
+ producer-app
+ producer-app
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Producer App
+
+
+ 3.2.6
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${springboot.version}
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+ ${dapr.sdk.alpha.version}
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ ${dapr.sdk.alpha.version}
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ postgresql
+ 1.20.0
+ test
+
+
+ org.testcontainers
+ rabbitmq
+ 1.20.0
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java
new file mode 100644
index 000000000..2211d1e90
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Customer.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+public class Customer {
+ private String customerName;
+ private String workflowId;
+ private boolean inCustomerDB = false;
+ private boolean followUp = false;
+
+ public boolean isFollowUp() {
+ return followUp;
+ }
+
+ public void setFollowUp(boolean followUp) {
+ this.followUp = followUp;
+ }
+
+ public boolean isInCustomerDB() {
+ return inCustomerDB;
+ }
+
+ public void setInCustomerDB(boolean inCustomerDB) {
+ this.inCustomerDB = inCustomerDB;
+ }
+
+ public String getWorkflowId() {
+ return workflowId;
+ }
+
+ public void setWorkflowId(String workflowId) {
+ this.workflowId = workflowId;
+ }
+
+ public String getCustomerName() {
+ return customerName;
+ }
+
+ public void setCustomerName(String customerName) {
+ this.customerName = customerName;
+ }
+
+ @Override
+ public String toString() {
+ return "Customer [customerName=" + customerName + ", workflowId=" + workflowId + ", inCustomerDB="
+ + inCustomerDB + ", followUp=" + followUp + "]";
+ }
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java
new file mode 100644
index 000000000..35c884e06
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomerStore.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class CustomerStore {
+ private Map customers = new HashMap<>();
+
+ public void addCustomer(Customer customer) {
+ customers.put(customer.getCustomerName(), customer);
+ }
+
+ public Customer getCustomer(String customerName) {
+ return customers.get(customerName);
+ }
+
+ public Collection getCustomers() {
+ return customers.values();
+ }
+
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java
new file mode 100644
index 000000000..57622d104
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/CustomersRestController.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import io.dapr.spring.workflows.config.EnableDaprWorkflows;
+import io.dapr.springboot.examples.producer.workflow.CustomerWorkflow;
+import io.dapr.workflows.client.DaprWorkflowClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@EnableDaprWorkflows
+public class CustomersRestController {
+
+
+ private final Logger logger = LoggerFactory.getLogger(CustomersRestController.class);
+
+ @Autowired
+ private DaprWorkflowClient daprWorkflowClient;
+
+ @Autowired
+ private CustomerStore customerStore;
+
+ @GetMapping("/")
+ public String root() {
+ return "OK";
+ }
+
+ private Map customersWorkflows = new HashMap<>();
+
+ /**
+ * Track customer endpoint.
+ *
+ * @param customer provided customer to track
+ * @return confirmation that the workflow instance was created for a given customer
+ */
+ @PostMapping("/customers")
+ public String trackCustomer(@RequestBody Customer customer) {
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(CustomerWorkflow.class, customer);
+ logger.info("Workflow instance " + instanceId + " started");
+ customersWorkflows.put(customer.getCustomerName(), instanceId);
+ return "New Workflow Instance created for Customer: " + customer.getCustomerName();
+ }
+
+ /**
+ * Request customer follow-up.
+ * @param customer associated with a workflow instance
+ * @return confirmation that the follow-up was requested
+ */
+ @PostMapping("/customers/followup")
+ public String customerNotification(@RequestBody Customer customer) {
+ logger.info("Customer follow-up requested: " + customer.getCustomerName());
+ String workflowIdForCustomer = customersWorkflows.get(customer.getCustomerName());
+ if (workflowIdForCustomer == null || workflowIdForCustomer.isEmpty()) {
+ return "There is no workflow associated with customer: " + customer.getCustomerName();
+ } else {
+ daprWorkflowClient.raiseEvent(workflowIdForCustomer, "CustomerReachOut", customer);
+ return "Customer Follow-up requested";
+ }
+ }
+
+
+ public Collection getCustomers() {
+ return customerStore.getCustomers();
+ }
+
+
+}
+
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java
new file mode 100644
index 000000000..820613037
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/Order.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import org.springframework.data.annotation.Id;
+
+public class Order {
+
+ @Id
+ private String id;
+ private String item;
+ private Integer amount;
+
+ public Order() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getItem() {
+ return item;
+ }
+
+ public void setItem(String item) {
+ this.item = item;
+ }
+
+ public Integer getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Integer amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public String toString() {
+ return "Order{" + "id='" + id + '\'' + ", item='" + item + '\'' + ", amount=" + amount + '}';
+ }
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java
new file mode 100644
index 000000000..1111f28db
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrderRepository.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.List;
+
+public interface OrderRepository extends CrudRepository {
+
+ List findByItem(String item);
+
+ List findByAmount(Integer amount);
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java
new file mode 100644
index 000000000..90384b8c0
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/OrdersRestController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import io.dapr.spring.data.repository.config.EnableDaprRepositories;
+import io.dapr.spring.messaging.DaprMessagingTemplate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@EnableDaprRepositories
+public class OrdersRestController {
+
+ private final Logger logger = LoggerFactory.getLogger(OrdersRestController.class);
+
+ @Autowired
+ private OrderRepository repository;
+
+ @Autowired
+ private DaprMessagingTemplate messagingTemplate;
+
+ /**
+ * Store orders from customers.
+ * @param order from the customer
+ *
+ * @return confirmation that the order was stored and the event published
+ */
+ @PostMapping("/orders")
+ public String storeOrder(@RequestBody Order order) {
+ logger.info("Storing Order: " + order);
+ repository.save(order);
+ logger.info("Publishing Order Event: " + order);
+ messagingTemplate.send("topic", order);
+ return "Order Stored and Event Published";
+ }
+
+ @GetMapping("/orders")
+ public Iterable getAll() {
+ return repository.findAll();
+ }
+
+ @GetMapping("/orders/byItem/")
+ public Iterable getAllByItem(@RequestParam("item") String item) {
+ return repository.findByItem(item);
+ }
+
+ @GetMapping("/orders/byAmount/")
+ public Iterable getAllByItem(@RequestParam("amount") Integer amount) {
+ return repository.findByAmount(amount);
+ }
+
+
+}
+
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java
new file mode 100644
index 000000000..18a3d237c
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerAppConfiguration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.dapr.client.DaprClient;
+import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties;
+import io.dapr.spring.boot.autoconfigure.statestore.DaprStateStoreProperties;
+import io.dapr.spring.data.DaprKeyValueAdapterResolver;
+import io.dapr.spring.data.DaprKeyValueTemplate;
+import io.dapr.spring.data.KeyValueAdapterResolver;
+import io.dapr.spring.messaging.DaprMessagingTemplate;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties({DaprPubSubProperties.class, DaprStateStoreProperties.class})
+public class ProducerAppConfiguration {
+ @Bean
+ public ObjectMapper mapper() {
+ return new ObjectMapper();
+ }
+
+
+ /**
+ * Produce a KeyValueAdapterResolver for Dapr.
+ * @param daprClient dapr client
+ * @param mapper object mapper
+ * @param daprStatestoreProperties properties to configure state store
+ * @return KeyValueAdapterResolver
+ */
+ @Bean
+ public KeyValueAdapterResolver keyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper,
+ DaprStateStoreProperties daprStatestoreProperties) {
+ String storeName = daprStatestoreProperties.getName();
+ String bindingName = daprStatestoreProperties.getBinding();
+
+ return new DaprKeyValueAdapterResolver(daprClient, mapper, storeName, bindingName);
+ }
+
+ @Bean
+ public DaprKeyValueTemplate daprKeyValueTemplate(KeyValueAdapterResolver keyValueAdapterResolver) {
+ return new DaprKeyValueTemplate(keyValueAdapterResolver);
+ }
+
+ @Bean
+ public DaprMessagingTemplate messagingTemplate(DaprClient daprClient,
+ DaprPubSubProperties daprPubSubProperties) {
+ return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
+ }
+
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java
new file mode 100644
index 000000000..9ff372714
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/ProducerApplication.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class ProducerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ProducerApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java
new file mode 100644
index 000000000..fb4e20770
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerFollowupActivity.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer.workflow;
+
+import io.dapr.springboot.examples.producer.Customer;
+import io.dapr.springboot.examples.producer.CustomerStore;
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CustomerFollowupActivity implements WorkflowActivity {
+
+ private final Logger logger = LoggerFactory.getLogger(CustomerFollowupActivity.class);
+
+ private final CustomerStore customerStore;
+
+ public CustomerFollowupActivity(CustomerStore customerStore) {
+ this.customerStore = customerStore;
+ }
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Customer customer = ctx.getInput(Customer.class);
+ //Let's get the hydrate the real customer from the CustomerStore
+ customer = customerStore.getCustomer(customer.getCustomerName());
+ customer.setFollowUp(true);
+ customerStore.addCustomer(customer);
+ logger.info("Customer: " + customer.getCustomerName() + " follow-up done.");
+ return customer;
+ }
+
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java
new file mode 100644
index 000000000..1e8757f99
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/CustomerWorkflow.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer.workflow;
+
+import io.dapr.springboot.examples.producer.Customer;
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+@Component
+public class CustomerWorkflow implements Workflow {
+
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ String instanceId = ctx.getInstanceId();
+ Customer customer = ctx.getInput(Customer.class);
+ customer.setWorkflowId(instanceId);
+ ctx.getLogger().info("Let's register the customer: " + customer.getCustomerName());
+ ctx.callActivity(RegisterCustomerActivity.class.getName(), customer, Customer.class).await();
+ ctx.getLogger().info("Let's wait for the customer: " + customer.getCustomerName() + " to request a follow up.");
+ customer = ctx.waitForExternalEvent("CustomerReachOut", Duration.ofMinutes(5), Customer.class).await();
+ ctx.getLogger().info("Let's book a follow up for the customer: " + customer.getCustomerName());
+ customer = ctx.callActivity(CustomerFollowupActivity.class.getName(), customer, Customer.class).await();
+ ctx.getLogger().info("Congratulations the customer: " + customer.getCustomerName() + " is happy!");
+ ctx.complete(customer);
+ };
+ }
+}
diff --git a/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java
new file mode 100644
index 000000000..c326c002c
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/java/io/dapr/springboot/examples/producer/workflow/RegisterCustomerActivity.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer.workflow;
+
+
+import io.dapr.springboot.examples.producer.Customer;
+import io.dapr.springboot.examples.producer.CustomerStore;
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class RegisterCustomerActivity implements WorkflowActivity {
+
+ private final Logger logger = LoggerFactory.getLogger(RegisterCustomerActivity.class);
+ private final CustomerStore customerStore;
+
+ public RegisterCustomerActivity(CustomerStore customerStore) {
+ this.customerStore = customerStore;
+ }
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Customer customer = ctx.getInput(Customer.class);
+ customer.setInCustomerDB(true);
+ logger.info("Customer: " + customer.getCustomerName() + " registered.");
+ customerStore.addCustomer(customer);
+ return customer;
+ }
+
+
+}
diff --git a/spring-boot-examples/producer-app/src/main/resources/application.properties b/spring-boot-examples/producer-app/src/main/resources/application.properties
new file mode 100644
index 000000000..1498965c7
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+spring.application.name=producer-app
+dapr.pubsub.name=pubsub
+dapr.statestore.name=kvstore
+dapr.statestore.binding=kvbinding
diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java
new file mode 100644
index 000000000..180f8badd
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/DaprTestContainersConfig.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import io.dapr.testcontainers.Component;
+import io.dapr.testcontainers.DaprContainer;
+import io.dapr.testcontainers.Subscription;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.containers.RabbitMQContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@TestConfiguration(proxyBeanMethods = false)
+public class DaprTestContainersConfig {
+
+ static final String CONNECTION_STRING =
+ "host=postgres user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository";
+ static final Map STATE_STORE_PROPERTIES = createStateStoreProperties();
+
+ static final Map BINDING_PROPERTIES = Collections.singletonMap("connectionString", CONNECTION_STRING);
+
+
+ @Bean
+ public Network getNetwork() {
+ Network defaultDaprNetwork = new Network() {
+ @Override
+ public String getId() {
+ return "dapr-network";
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return null;
+ }
+ };
+
+ List networks = DockerClientFactory.instance().client().listNetworksCmd().withNameFilter("dapr-network").exec();
+ if (networks.isEmpty()) {
+ Network.builder()
+ .createNetworkCmdModifier(cmd -> cmd.withName("dapr-network"))
+ .build().getId();
+ return defaultDaprNetwork;
+ } else {
+ return defaultDaprNetwork;
+ }
+ }
+
+
+ @Bean
+ public RabbitMQContainer rabbitMQContainer(Network daprNetwork, Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine"))
+ .withExposedPorts(5672)
+ .withNetworkAliases("rabbitmq")
+ .withReuse(true)
+ .withNetwork(daprNetwork);
+
+ }
+
+ @Bean
+ public PostgreSQLContainer> postgreSQLContainer(Network daprNetwork) {
+ return new PostgreSQLContainer<>("postgres:16-alpine")
+ .withNetworkAliases("postgres")
+ .withDatabaseName("dapr_db_repository")
+ .withUsername("postgres")
+ .withPassword("password")
+ .withExposedPorts(5432)
+ .withNetwork(daprNetwork);
+
+ }
+
+ @Bean
+ @ServiceConnection
+ public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer> postgreSQLContainer, RabbitMQContainer rabbitMQContainer) {
+
+ Map rabbitMqProperties = new HashMap<>();
+ rabbitMqProperties.put("connectionString", "amqp://guest:guest@rabbitmq:5672");
+ rabbitMqProperties.put("user", "guest");
+ rabbitMqProperties.put("password", "guest");
+
+ return new DaprContainer("daprio/daprd:1.14.4")
+ .withAppName("producer-app")
+ .withNetwork(daprNetwork)
+ .withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES))
+ .withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES))
+ .withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties))
+ .withSubscription(new Subscription("app", "pubsub", "topic", "/subscribe"))
+// .withDaprLogLevel(DaprLogLevel.DEBUG)
+// .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
+ .withAppPort(8080)
+ .withAppHealthCheckPath("/actuator/health")
+ .withAppChannelAddress("host.testcontainers.internal")
+ .dependsOn(rabbitMQContainer)
+ .dependsOn(postgreSQLContainer);
+ }
+
+
+ private static Map createStateStoreProperties() {
+ Map result = new HashMap<>();
+
+ result.put("keyPrefix", "name");
+ result.put("actorStateStore", String.valueOf(true));
+ result.put("connectionString", CONNECTION_STRING);
+
+ return result;
+ }
+
+
+}
diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java
new file mode 100644
index 000000000..47e5eb486
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import io.dapr.client.DaprClient;
+import io.dapr.springboot.DaprAutoConfiguration;
+import io.dapr.springboot.examples.producer.workflow.CustomerFollowupActivity;
+import io.dapr.springboot.examples.producer.workflow.CustomerWorkflow;
+import io.dapr.springboot.examples.producer.workflow.RegisterCustomerActivity;
+import io.dapr.testcontainers.DaprContainer;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+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.testcontainers.containers.wait.strategy.Wait;
+
+import java.io.IOException;
+import java.time.Duration;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(classes = {TestProducerApplication.class, DaprTestContainersConfig.class,
+ DaprAutoConfiguration.class, CustomerWorkflow.class, CustomerFollowupActivity.class,
+ RegisterCustomerActivity.class, CustomerStore.class},
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+class ProducerAppTests {
+
+ private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
+
+ @Autowired
+ private TestSubscriberRestController controller;
+
+ @Autowired
+ private CustomerStore customerStore;
+
+ @Autowired
+ private DaprClient daprClient;
+
+
+ @Autowired
+ private DaprContainer daprContainer;
+
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + 8080;
+ org.testcontainers.Testcontainers.exposeHostPorts(8080);
+ // Ensure the subscriptions are registered
+ Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(daprContainer);
+
+ }
+
+
+ @Test
+ void testOrdersEndpointAndMessaging() throws InterruptedException, IOException {
+
+ given().contentType(ContentType.JSON)
+ .body("{ \"id\": \"abc-123\",\"item\": \"the mars volta LP\",\"amount\": 1}")
+ .when()
+ .post("/orders")
+ .then()
+ .statusCode(200);
+
+ await().atMost(Duration.ofSeconds(15))
+ .until(controller.getAllEvents()::size, equalTo(1));
+
+ given().contentType(ContentType.JSON)
+ .when()
+ .get("/orders")
+ .then()
+ .statusCode(200).body("size()", is(1));
+
+ given().contentType(ContentType.JSON)
+ .when()
+ .queryParam("item", "the mars volta LP")
+ .get("/orders/byItem/")
+ .then()
+ .statusCode(200).body("size()", is(1));
+
+ given().contentType(ContentType.JSON)
+ .when()
+ .queryParam("item", "other")
+ .get("/orders/byItem/")
+ .then()
+ .statusCode(200).body("size()", is(0));
+
+ given().contentType(ContentType.JSON)
+ .when()
+ .queryParam("amount", 1)
+ .get("/orders/byAmount/")
+ .then()
+ .statusCode(200).body("size()", is(1));
+
+ given().contentType(ContentType.JSON)
+ .when()
+ .queryParam("amount", 2)
+ .get("/orders/byAmount/")
+ .then()
+ .statusCode(200).body("size()", is(0));
+
+ }
+
+ @Test
+ void testCustomersWorkflows() throws InterruptedException, IOException {
+
+ given().contentType(ContentType.JSON)
+ .body("{\"customerName\": \"salaboy\"}")
+ .when()
+ .post("/customers")
+ .then()
+ .statusCode(200);
+
+
+ await().atMost(Duration.ofSeconds(15))
+ .until(customerStore.getCustomers()::size, equalTo(1));
+ Customer customer = customerStore.getCustomer("salaboy");
+ assertEquals(true, customer.isInCustomerDB());
+ String workflowId = customer.getWorkflowId();
+ given().contentType(ContentType.JSON)
+ .body("{ \"workflowId\": \"" + workflowId + "\",\"customerName\": \"salaboy\" }")
+ .when()
+ .post("/customers/followup")
+ .then()
+ .statusCode(200);
+
+ assertEquals(1, customerStore.getCustomers().size());
+
+ await().atMost(Duration.ofSeconds(10))
+ .until(customerStore.getCustomer("salaboy")::isFollowUp, equalTo(true));
+
+ }
+
+}
diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java
new file mode 100644
index 000000000..0abb4c929
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestProducerApplication.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class TestProducerApplication {
+
+ public static void main(String[] args) {
+
+ SpringApplication.from(ProducerApplication::main)
+ .with(DaprTestContainersConfig.class)
+ .run(args);
+ org.testcontainers.Testcontainers.exposeHostPorts(8080);
+ }
+
+}
diff --git a/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java
new file mode 100644
index 000000000..0f39dd9a9
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/TestSubscriberRestController.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.producer;
+
+import io.dapr.Topic;
+import io.dapr.client.domain.CloudEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+public class TestSubscriberRestController {
+
+ private List events = new ArrayList<>();
+
+ private final Logger logger = LoggerFactory.getLogger(TestSubscriberRestController.class);
+
+ @PostMapping("subscribe")
+ @Topic(pubsubName = "pubsub", name = "topic")
+ public void subscribe(@RequestBody CloudEvent cloudEvent){
+ logger.info("Order Event Received: " + cloudEvent.getData());
+ events.add(cloudEvent);
+ }
+
+ public List getAllEvents() {
+ return events;
+ }
+}
+
diff --git a/spring-boot-examples/producer-app/src/test/resources/application.properties b/spring-boot-examples/producer-app/src/test/resources/application.properties
new file mode 100644
index 000000000..2429abb69
--- /dev/null
+++ b/spring-boot-examples/producer-app/src/test/resources/application.properties
@@ -0,0 +1,3 @@
+dapr.statestore.name=kvstore
+dapr.statestore.binding=kvbinding
+dapr.pubsub.name=pubsub
diff --git a/spring-boot-examples/spotbugs-exclude.xml b/spring-boot-examples/spotbugs-exclude.xml
new file mode 100644
index 000000000..264fc79b0
--- /dev/null
+++ b/spring-boot-examples/spotbugs-exclude.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+