diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c89b2f9a84..b97c9717d5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -30,7 +30,7 @@ jobs:
distribution: 'temurin'
java-version: ${{ env.JDK_VER }}
- name: Run tests
- run: ./mvnw clean install -B -q
+ run: ./mvnw clean install -B -q -DskipITs=true
- name: Codecov
uses: codecov/codecov-action@v5.5.1
- name: Upload test report for sdk
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 0a73978c7a..e73bcf4a1d 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -123,8 +123,8 @@ jobs:
mm.py README.md
env:
DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
- - name: Validate Spring Boot Workflow examples
- working-directory: ./spring-boot-examples/workflows
+ - name: Validate Spring Boot Workflow Patterns examples
+ working-directory: ./spring-boot-examples/workflows/patterns
run: |
mm.py README.md
env:
diff --git a/pom.xml b/pom.xml
index 0c812dad80..8c9caa57e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,7 +42,7 @@
true
../spotbugs-exclude.xml
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED
- 3.2.2
+ 3.5.3
3.2.2
5.11.4
2.0
@@ -665,6 +665,7 @@
integration-tests
sdk-tests
+ spring-boot-examples
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/ConsumerAppIT.java
similarity index 99%
rename from spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppTests.java
rename to spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/ConsumerAppIT.java
index e7e6f0c032..5334f49520 100644
--- 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/ConsumerAppIT.java
@@ -37,7 +37,7 @@
@SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class,
ConsumerAppTestConfiguration.class, DaprAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
-class ConsumerAppTests {
+class ConsumerAppIT {
private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml
index 8849630b1a..69cbd693e6 100644
--- a/spring-boot-examples/pom.xml
+++ b/spring-boot-examples/pom.xml
@@ -52,6 +52,23 @@
true
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+ ${project.build.outputDirectory}
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+ **/*io/dapr/springboot/examples/**
+
+
+
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/ProducerAppIT.java
similarity index 99%
rename from spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppTests.java
rename to spring-boot-examples/producer-app/src/test/java/io/dapr/springboot/examples/producer/ProducerAppIT.java
index 47e5eb4862..24fa34c6fb 100644
--- 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/ProducerAppIT.java
@@ -40,7 +40,7 @@
DaprAutoConfiguration.class, CustomerWorkflow.class, CustomerFollowupActivity.class,
RegisterCustomerActivity.class, CustomerStore.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
-class ProducerAppTests {
+class ProducerAppIT {
private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";
diff --git a/spring-boot-examples/workflows/multi-app/README.md b/spring-boot-examples/workflows/multi-app/README.md
new file mode 100644
index 0000000000..108cdfe7ce
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/README.md
@@ -0,0 +1,91 @@
+# Multi App workflow Example
+
+This example demonstrates how you can create distributed workflows where the orchestrator doesn't host the workflow activities.
+
+For more documentation about how Multi App Workflows work [check the official documentation here](https://v1-16.docs.dapr.io/developing-applications/building-blocks/workflow/workflow-multi-app/).
+
+This example is composed by three Spring Boot applications:
+- `orchestrator`: The `orchestrator` app contains the Dapr Workflow definition and expose REST endpoints to create and raise events against workflow instances.
+- `worker-one`: The `worker-one` app contains the `RegisterCustomerActivity` definition, which will be called by the `orchestrator` app.
+- `worker-two`: The `worker-two` app contains the `CustomerFollowupActivity` definition, which will be called by the `orchestrator` app.
+
+To start the applications you need to run the following commands on separate terminals, starting from the `multi-app` directory.
+To start the `orchestrator` app run:
+```bash
+cd orchestrator/
+mvn -Dspring-boot.run.arguments="--reuse=true" clean spring-boot:test-run
+```
+
+The `orchestrator` application will run on port `8080`.
+
+On a separate terminal, to start the `worker-one` app run:
+```bash
+cd worker-one/
+mvn -Dspring-boot.run.arguments="--reuse=true" clean spring-boot:test-run
+```
+
+The `worker-one` application will run on port `8081`.
+
+On a separate terminal, to start the `worker-two` app run:
+```bash
+cd worker-two/
+mvn -Dspring-boot.run.arguments="--reuse=true" clean spring-boot:test-run
+```
+
+The `worker-two` application will run on port `8082`.
+
+You can create new workflow instances of the `CustomerWorkflow` by calling the `/customers` endpoint of the `orchestrator` application.
+
+```bash
+curl -X POST localhost:8080/customers -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
+```
+
+The workflow definition [`CustomerWorkflow`](orchstrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java) that you can find inside the `orchestrator` app,
+performs the following orchestration when a new workflow instance is created:
+
+- Call the `RegisterCustomerActivity` activity which can be found inside the `worker-one` application.
+ - You can find in the workflow definition the configuration to make reference to an Activity that is hosted by a different Dapr application.
+ ```java
+ customer = ctx.callActivity("io.dapr.springboot.examples.workerone.RegisterCustomerActivity",
+ customer,
+ new WorkflowTaskOptions("worker-one"),
+ Customer.class).
+ await();
+ ```
+- Wait for an external event of type `CustomerReachOut` with a timeout of 5 minutes:
+ ```java
+ ctx.waitForExternalEvent("CustomerReachOut", Duration.ofMinutes(5), Customer.class).await();
+ ```
+- You can check the status of the workflow for a given customer by sending the following request:
+ ```shell
+ curl -X POST localhost:8080/customers/status -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
+ ```
+- You can call the following endpoint on the `orchestrator` app to raise the external event:
+ ```shell
+ curl -X POST localhost:8080/customers/followup -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
+ ```
+- When the event is received, the workflow move forward to the last activity called `CustomerFollowUpActivity`, that can be found on the `worker-two` app.
+ ```java
+ customer = ctx.callActivity("io.dapr.springboot.examples.workertwo.CustomerFollowupActivity",
+ customer,
+ new WorkflowTaskOptions("worker-two"),
+ Customer.class).
+ await();
+ ```
+- The workflow completes by handing out the final version of the `Customer` object that has been modified the workflow activities. You can retrieve the `Customer` payload
+ by running the following command:
+ ```shell
+ curl -X POST localhost:8080/customers/output -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
+ ```
+
+## Testing Multi App Workflows
+
+Testing becomes a complex task when you are dealing with multiple Spring Boot applications. For testing this workflow,
+we rely on [Testcontainers](https://testcontainers.com) to create the entire setup which enable us to run the workflow end to end.
+
+You can find the end-to-end test in the [`OrchestratorAppIT.java`](orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/OrchestratorAppIT.java) class inside the `orchestrator` application.
+This test interact with the application REST endpoints to validate their correct execution.
+
+But the magic behind the test can be located in the [`DaprTestContainersConfig.class`](orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/DaprTestContainersConfig.java) which defines the configuration for
+all the Dapr containers and the `worker-one` and `worker-two` applications. Check this class to gain a deeper understand how to configure
+multiple Dapr-enabled applications.
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/pom.xml b/spring-boot-examples/workflows/multi-app/orchestrator/pom.xml
new file mode 100644
index 0000000000..c5907d373c
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ multi-app
+ 1.17.0-SNAPSHOT
+
+
+ orchestrator
+ orchestrator
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Orchestrator App
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ test
+
+
+ com.redis
+ testcontainers-redis
+ 2.2.2
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ true
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/Customer.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/Customer.java
new file mode 100644
index 0000000000..55fd25dc99
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/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.orchestrator;
+
+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/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java
new file mode 100644
index 0000000000..0e124d4a41
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.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.orchestrator;
+
+import io.dapr.durabletask.TaskCanceledException;
+import io.dapr.durabletask.TaskFailedException;
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import io.dapr.workflows.WorkflowTaskOptions;
+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());
+
+ customer = ctx.callActivity("io.dapr.springboot.examples.workerone.RegisterCustomerActivity", customer,
+ new WorkflowTaskOptions("worker-one"), Customer.class).await();
+
+ ctx.getLogger().info("Let's wait for the customer: {} to request a follow up.", customer.getCustomerName());
+ 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("io.dapr.springboot.examples.workertwo.CustomerFollowupActivity",
+ customer, new WorkflowTaskOptions("worker-two"), Customer.class).await();
+
+ ctx.getLogger().info("Congratulations the customer: {} is happy!", customer.getCustomerName());
+
+ ctx.getLogger().info("Final customer: {} ", customer);
+ ctx.complete(customer);
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomersRestController.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomersRestController.java
new file mode 100644
index 0000000000..46277ada97
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomersRestController.java
@@ -0,0 +1,110 @@
+/*
+ * 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.orchestrator;
+
+import io.dapr.spring.workflows.config.EnableDaprWorkflows;
+import io.dapr.workflows.client.DaprWorkflowClient;
+import io.dapr.workflows.client.WorkflowInstanceStatus;
+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.HashMap;
+import java.util.Map;
+
+@RestController
+public class CustomersRestController {
+
+ private final Logger logger = LoggerFactory.getLogger(CustomersRestController.class);
+
+ @Autowired
+ private DaprWorkflowClient daprWorkflowClient;
+
+ @GetMapping("/")
+ public String root() {
+ return "OK";
+ }
+
+ private final 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 {} started", instanceId);
+ 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();
+ }
+ daprWorkflowClient.raiseEvent(workflowIdForCustomer, "CustomerReachOut", customer);
+ return "Customer Follow-up requested";
+ }
+
+ /**
+ * Request customer workflow instance status.
+ * @param customer associated with a workflow instance
+ * @return the workflow instance status for a given customer
+ */
+ @PostMapping("/customers/status")
+ public String getCustomerStatus(@RequestBody Customer customer) {
+ logger.info("Customer status requested: {}", customer.getCustomerName());
+ String workflowIdForCustomer = customersWorkflows.get(customer.getCustomerName());
+ if (workflowIdForCustomer == null || workflowIdForCustomer.isEmpty()) {
+ return "N/A";
+ }
+ WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(workflowIdForCustomer, true);
+ assert instanceState != null;
+ return "Workflow for Customer: " + customer.getCustomerName() + " is " + instanceState.getRuntimeStatus().name();
+ }
+
+ /**
+ * Request customer output.
+ * @param customer associated with a workflow instance
+ * @return Customer status after the workflow execution finished
+ */
+ @PostMapping("/customers/output")
+ public Customer getCustomerOutput(@RequestBody Customer customer) {
+ logger.info("Customer output requested: {}", customer.getCustomerName());
+ String workflowIdForCustomer = customersWorkflows.get(customer.getCustomerName());
+ if (workflowIdForCustomer == null || workflowIdForCustomer.isEmpty()) {
+ return null;
+ }
+ WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(workflowIdForCustomer, true);
+ assert instanceState != null;
+ return instanceState.readOutputAs(Customer.class);
+ }
+
+}
+
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/OrchestratorApplication.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/OrchestratorApplication.java
new file mode 100644
index 0000000000..8ba4ec897e
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/java/io/dapr/springboot/examples/orchestrator/OrchestratorApplication.java
@@ -0,0 +1,29 @@
+/*
+ * 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.orchestrator;
+
+import io.dapr.spring.workflows.config.EnableDaprWorkflows;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+@EnableDaprWorkflows
+public class OrchestratorApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrchestratorApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/main/resources/application.properties b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/resources/application.properties
new file mode 100644
index 0000000000..3d555d71e5
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=orchestrator
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/DaprTestContainersConfig.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/DaprTestContainersConfig.java
new file mode 100644
index 0000000000..efdb511c8f
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/DaprTestContainersConfig.java
@@ -0,0 +1,214 @@
+/*
+ * 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.orchestrator;
+
+import com.redis.testcontainers.RedisContainer;
+import io.dapr.testcontainers.*;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+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.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static io.dapr.testcontainers.DaprContainerConstants.*;
+
+@TestConfiguration(proxyBeanMethods = false)
+public class DaprTestContainersConfig {
+
+ @Bean
+ public Network getDaprNetwork(Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ if (reuse) {
+ 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;
+ }
+ } else {
+ return Network.newNetwork();
+ }
+ }
+
+ private Map getRedisProps(){
+ Map redisProps = new HashMap<>();
+ redisProps.put("redisHost", "redis:6379");
+ redisProps.put("redisPassword", "");
+ redisProps.put("actorStateStore", String.valueOf(true));
+ return redisProps;
+ }
+
+ @Bean
+ public DaprPlacementContainer placementContainer(Network daprNetwork, Environment env){
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new DaprPlacementContainer(DockerImageName.parse(DAPR_PLACEMENT_IMAGE_TAG))
+ .withNetwork(daprNetwork)
+ .withReuse(reuse)
+ .withNetworkAliases("placement");
+ }
+
+ @Bean
+ public DaprSchedulerContainer schedulerContainer(Network daprNetwork, Environment env){
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new DaprSchedulerContainer(DockerImageName.parse(DAPR_SCHEDULER_IMAGE_TAG))
+ .withNetwork(daprNetwork)
+ .withReuse(reuse)
+ .withNetworkAliases("scheduler");
+ }
+
+ @Bean("workerOneDapr")
+ @ConditionalOnProperty(prefix = "tests", name = "workers.enabled", havingValue = "true")
+ public DaprContainer workerOneDapr(Network daprNetwork, RedisContainer redisContainer, Environment env,
+ DaprPlacementContainer daprPlacementContainer,
+ DaprSchedulerContainer daprSchedulerContainer) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("worker-one")
+ .withNetworkAliases("worker-one")
+ .withNetwork(daprNetwork)
+ .withPlacementContainer(daprPlacementContainer)
+ .withSchedulerContainer(daprSchedulerContainer)
+ .withComponent(new Component("kvstore", "state.redis", "v1", getRedisProps()))
+ .dependsOn(daprPlacementContainer)
+ .dependsOn(daprSchedulerContainer)
+ .dependsOn(redisContainer);
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = "tests", name = "workers.enabled", havingValue = "true")
+ public GenericContainer> workerOneContainer(Network daprNetwork,
+ @Qualifier("workerOneDapr") DaprContainer workerOneDapr,
+ DaprPlacementContainer daprPlacementContainer,
+ DaprSchedulerContainer daprSchedulerContainer){
+ return new GenericContainer<>("openjdk:17-jdk-slim")
+ .withCopyFileToContainer(MountableFile.forHostPath("../worker-one/target"), "/app")
+ .withWorkingDirectory("/app")
+ .withCommand("java",
+ "-Ddapr.grpc.endpoint=worker-one:50001",
+ "-Ddapr.http.endpoint=worker-one:3500",
+ "-jar",
+ "worker-one.jar")
+ .withNetwork(daprNetwork)
+ .dependsOn(workerOneDapr)
+ .dependsOn(daprPlacementContainer)
+ .dependsOn(daprSchedulerContainer)
+ .waitingFor(Wait.forLogMessage(".*Started WorkerOneApplication.*", 1))
+ .withLogConsumer(outputFrame -> System.out.println("WorkerOneApplication: " + outputFrame.getUtf8String()));
+ }
+
+ @Bean("workerTwoDapr")
+ @ConditionalOnProperty(prefix = "tests", name = "workers.enabled", havingValue = "true")
+ public DaprContainer workerTwoDapr(Network daprNetwork, RedisContainer redisContainer,
+ Environment env,
+ DaprPlacementContainer daprPlacementContainer,
+ DaprSchedulerContainer daprSchedulerContainer) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("worker-two")
+ .withNetworkAliases("worker-two")
+ .withNetwork(daprNetwork)
+ .withPlacementContainer(daprPlacementContainer)
+ .withSchedulerContainer(daprSchedulerContainer)
+ .withComponent(new Component("kvstore", "state.redis", "v1", getRedisProps()))
+ .dependsOn(daprPlacementContainer)
+ .dependsOn(daprSchedulerContainer)
+ .dependsOn(redisContainer);
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = "tests", name = "workers.enabled", havingValue = "true")
+ public GenericContainer> workerTwoContainer(Network daprNetwork,
+ @Qualifier("workerTwoDapr") DaprContainer workerTwoDapr,
+ DaprPlacementContainer daprPlacementContainer,
+ DaprSchedulerContainer daprSchedulerContainer){
+ return new GenericContainer<>("openjdk:17-jdk-slim")
+ .withCopyFileToContainer(MountableFile.forHostPath("../worker-two/target"), "/app")
+ .withWorkingDirectory("/app")
+ .withCommand("java",
+ "-Ddapr.grpc.endpoint=worker-two:50001",
+ "-Ddapr.http.endpoint=worker-two:3500",
+ "-jar",
+ "worker-two.jar")
+ .withNetwork(daprNetwork)
+ .dependsOn(workerTwoDapr)
+ .dependsOn(daprPlacementContainer)
+ .dependsOn(daprSchedulerContainer)
+ .waitingFor(Wait.forLogMessage(".*Started WorkerTwoApplication.*", 1))
+ .withLogConsumer(outputFrame -> System.out.println("WorkerTwoApplication: " + outputFrame.getUtf8String()));
+ }
+
+ @Bean
+ public RedisContainer redisContainer(Network daprNetwork, Environment env){
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME)
+ .withNetwork(daprNetwork)
+ .withReuse(reuse)
+ .withNetworkAliases("redis");
+ }
+
+ @Bean
+ @ServiceConnection
+ public DaprContainer daprContainer(Network daprNetwork, RedisContainer redisContainer,
+ Environment env,
+ DaprPlacementContainer daprPlacementContainer,
+ DaprSchedulerContainer daprSchedulerContainer) {
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("orchestrator")
+ .withNetwork(daprNetwork)
+ .withPlacementContainer(daprPlacementContainer)
+ .withSchedulerContainer(daprSchedulerContainer)
+ .withComponent(new Component("kvstore", "state.redis", "v1", getRedisProps()))
+ .withAppPort(8080)
+ .withAppHealthCheckPath("/actuator/health")
+ .withAppChannelAddress("host.testcontainers.internal")
+ .dependsOn(daprPlacementContainer)
+ .dependsOn(daprSchedulerContainer)
+ .dependsOn(redisContainer);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/OrchestratorAppIT.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/OrchestratorAppIT.java
new file mode 100644
index 0000000000..5113256d90
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/OrchestratorAppIT.java
@@ -0,0 +1,104 @@
+/*
+ * 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.orchestrator;
+
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+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.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SpringBootTest(classes = {TestOrchestratorApplication.class, DaprTestContainersConfig.class, CustomersRestController.class},
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
+ properties = {"reuse=false", "tests.workers.enabled=true"})
+class OrchestratorAppIT {
+
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + 8080;
+ org.testcontainers.Testcontainers.exposeHostPorts(8080);
+
+ }
+
+ @Test
+ void testCustomersWorkflows() throws InterruptedException, IOException {
+
+ // Create a new workflow instance for a given customer
+ given().contentType(ContentType.JSON)
+ .body("{\"customerName\": \"salaboy\"}")
+ .when()
+ .post("/customers")
+ .then()
+ .statusCode(200);
+
+ // Wait for the workflow instance to be running by checking the status
+ await().atMost(Duration.ofSeconds(5)).until(() ->
+ {
+ String workflowStatus = given().contentType(ContentType.JSON)
+ .body("{\"customerName\": \"salaboy\" }")
+ .when()
+ .post("/customers/status")
+ .then()
+ .statusCode(200)
+ .extract().asString();
+ return workflowStatus.equals("Workflow for Customer: salaboy is RUNNING");
+ }
+ );
+
+ // Raise an external event to move the workflow forward
+ given().contentType(ContentType.JSON)
+ .body("{\"customerName\": \"salaboy\" }")
+ .when()
+ .post("/customers/followup")
+ .then()
+ .statusCode(200);
+
+ // Wait for the workflow instance to be completed by checking the status
+ await().atMost(Duration.ofSeconds(5)).until(() ->
+ {
+ String workflowStatus = given().contentType(ContentType.JSON)
+ .body("{\"customerName\": \"salaboy\" }")
+ .when()
+ .post("/customers/status")
+ .then()
+ .statusCode(200).extract().asString();
+ return workflowStatus.equals("Workflow for Customer: salaboy is COMPLETED");
+ }
+ );
+
+ // Get the customer after running all the workflow activities
+ Customer customer = given().contentType(ContentType.JSON)
+ .body("{\"customerName\": \"salaboy\" }")
+ .when()
+ .post("/customers/output")
+ .then()
+ .statusCode(200).extract().as(Customer.class);
+
+ assertTrue(customer.isInCustomerDB());
+ assertTrue(customer.isFollowUp());
+
+
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/TestOrchestratorApplication.java b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/TestOrchestratorApplication.java
new file mode 100644
index 0000000000..9e2b4be77a
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/TestOrchestratorApplication.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.orchestrator;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class TestOrchestratorApplication {
+
+ public static void main(String[] args) {
+
+ SpringApplication.from(OrchestratorApplication::main)
+ .with(DaprTestContainersConfig.class)
+ .run(args);
+ org.testcontainers.Testcontainers.exposeHostPorts(8080);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/orchestrator/src/test/resources/application.properties b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/resources/application.properties
new file mode 100644
index 0000000000..ce86cb5399
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/orchestrator/src/test/resources/application.properties
@@ -0,0 +1,2 @@
+dapr.statestore.name=kvstore
+server.port=8080
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/multi-app/pom.xml b/spring-boot-examples/workflows/multi-app/pom.xml
new file mode 100644
index 0000000000..6586459001
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ io.dapr
+ workflows
+ 1.17.0-SNAPSHOT
+
+
+ multi-app
+ 1.17.0-SNAPSHOT
+ pom
+
+
+ true
+
+
+
+ orchestrator
+ worker-one
+ worker-two
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${springboot.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ true
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/multi-app/spotbugs-exclude.xml b/spring-boot-examples/workflows/multi-app/spotbugs-exclude.xml
new file mode 100644
index 0000000000..264fc79b0a
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/spotbugs-exclude.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/pom.xml b/spring-boot-examples/workflows/multi-app/worker-one/pom.xml
new file mode 100644
index 0000000000..88c5fec997
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ multi-app
+ 1.17.0-SNAPSHOT
+
+
+ worker-one
+ worker-one
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Worker 1 App
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ test
+
+
+ com.redis
+ testcontainers-redis
+ 2.2.2
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ worker-one
+
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ true
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/Customer.java b/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/Customer.java
new file mode 100644
index 0000000000..88d24783dc
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/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.workerone;
+
+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/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/RegisterCustomerActivity.java b/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/RegisterCustomerActivity.java
new file mode 100644
index 0000000000..001c9e4489
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/RegisterCustomerActivity.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.workerone;
+
+
+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);
+
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Customer customer = ctx.getInput(Customer.class);
+ customer.setInCustomerDB(true);
+ logger.info("Customer: {} registered.", customer.getCustomerName());
+ return customer;
+ }
+
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/WorkerOneApplication.java b/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/WorkerOneApplication.java
new file mode 100644
index 0000000000..552a5fc816
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/main/java/io/dapr/springboot/examples/workerone/WorkerOneApplication.java
@@ -0,0 +1,29 @@
+/*
+ * 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.workerone;
+
+import io.dapr.spring.workflows.config.EnableDaprWorkflows;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+@EnableDaprWorkflows
+public class WorkerOneApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WorkerOneApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/main/resources/application.properties b/spring-boot-examples/workflows/multi-app/worker-one/src/main/resources/application.properties
new file mode 100644
index 0000000000..324435fa52
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+spring.application.name=worker-one
+server.port=8081
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/DaprTestContainersConfig.java b/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/DaprTestContainersConfig.java
new file mode 100644
index 0000000000..e63c89687f
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/DaprTestContainersConfig.java
@@ -0,0 +1,102 @@
+/*
+ * 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.workerone;
+
+import com.redis.testcontainers.RedisContainer;
+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 java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
+
+@TestConfiguration(proxyBeanMethods = false)
+public class DaprTestContainersConfig {
+
+ @Bean
+ public Network getDaprNetwork(Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ if (reuse) {
+ 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;
+ }
+ } else {
+ return Network.newNetwork();
+ }
+ }
+
+ @Bean
+ public RedisContainer redisContainer(Network daprNetwork, Environment env){
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME)
+ .withNetwork(daprNetwork)
+ .withReuse(reuse)
+ .withNetworkAliases("redis");
+ }
+
+ @Bean
+ @ServiceConnection
+ public DaprContainer daprContainer(Network daprNetwork, RedisContainer redisContainer, Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ Map redisProps = new HashMap<>();
+ redisProps.put("redisHost", "redis:6379");
+ redisProps.put("redisPassword", "");
+ redisProps.put("actorStateStore", String.valueOf(true));
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("worker-one")
+ .withNetwork(daprNetwork)
+ .withReusablePlacement(reuse)
+ .withReusableScheduler(reuse)
+ .withComponent(new Component("kvstore", "state.redis", "v1", redisProps))
+ .withAppPort(8081)
+ .withAppHealthCheckPath("/actuator/health")
+ .withAppChannelAddress("host.testcontainers.internal")
+ .dependsOn(redisContainer);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/TestWorkerOneApplication.java b/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/TestWorkerOneApplication.java
new file mode 100644
index 0000000000..4e285ca0bf
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/TestWorkerOneApplication.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.workerone;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class TestWorkerOneApplication {
+
+ public static void main(String[] args) {
+
+ SpringApplication.from(WorkerOneApplication::main)
+ .with(DaprTestContainersConfig.class)
+ .run(args);
+ org.testcontainers.Testcontainers.exposeHostPorts(8081);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/WorkerOneAppIT.java b/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/WorkerOneAppIT.java
new file mode 100644
index 0000000000..a4a9cbc4d9
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/test/java/io/dapr/springboot/examples/workerone/WorkerOneAppIT.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.workerone;
+
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(classes = {TestWorkerOneApplication.class, DaprTestContainersConfig.class},
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+class WorkerOneAppIT {
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + 8081;
+ org.testcontainers.Testcontainers.exposeHostPorts(8081);
+
+ }
+
+ @Test
+ void testWorkerOne() {
+ //Test the logic of the worker one
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-one/src/test/resources/application.properties b/spring-boot-examples/workflows/multi-app/worker-one/src/test/resources/application.properties
new file mode 100644
index 0000000000..f1b6b99450
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-one/src/test/resources/application.properties
@@ -0,0 +1,2 @@
+dapr.statestore.name=kvstore
+server.port=8081
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/pom.xml b/spring-boot-examples/workflows/multi-app/worker-two/pom.xml
new file mode 100644
index 0000000000..508a725ecb
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ multi-app
+ 1.17.0-SNAPSHOT
+
+
+ worker-two
+ worker-two
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Worker 2 App
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ test
+
+
+ com.redis
+ testcontainers-redis
+ 2.2.2
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ worker-two
+
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ true
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/Customer.java b/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/Customer.java
new file mode 100644
index 0000000000..5ba027cc04
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/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.workertwo;
+
+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/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/CustomerFollowupActivity.java b/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/CustomerFollowupActivity.java
new file mode 100644
index 0000000000..76c042ecd2
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/CustomerFollowupActivity.java
@@ -0,0 +1,35 @@
+/*
+ * 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.workertwo;
+
+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);
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ Customer customer = ctx.getInput(Customer.class);
+ customer.setFollowUp(true);
+ logger.info("Customer: {} follow up scheduled.", customer.getCustomerName());
+ return customer;
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/WorkerTwoApplication.java b/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/WorkerTwoApplication.java
new file mode 100644
index 0000000000..3adaaeae64
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/main/java/io/dapr/springboot/examples/workertwo/WorkerTwoApplication.java
@@ -0,0 +1,29 @@
+/*
+ * 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.workertwo;
+
+import io.dapr.spring.workflows.config.EnableDaprWorkflows;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+@EnableDaprWorkflows
+public class WorkerTwoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WorkerTwoApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/main/resources/application.properties b/spring-boot-examples/workflows/multi-app/worker-two/src/main/resources/application.properties
new file mode 100644
index 0000000000..7857e2cad1
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+spring.application.name=worker-two
+server.port=8082
\ No newline at end of file
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/DaprTestContainersConfig.java b/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/DaprTestContainersConfig.java
new file mode 100644
index 0000000000..e63ab0cf7e
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/DaprTestContainersConfig.java
@@ -0,0 +1,102 @@
+/*
+ * 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.workertwo;
+
+import com.redis.testcontainers.RedisContainer;
+import io.dapr.testcontainers.Component;
+import io.dapr.testcontainers.DaprContainer;
+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 java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
+
+@TestConfiguration(proxyBeanMethods = false)
+public class DaprTestContainersConfig {
+
+ @Bean
+ public Network getDaprNetwork(Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ if (reuse) {
+ 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;
+ }
+ } else {
+ return Network.newNetwork();
+ }
+ }
+
+
+ @Bean
+ public RedisContainer redisContainer(Network daprNetwork, Environment env){
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ return new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME)
+ .withNetwork(daprNetwork)
+ .withReuse(reuse)
+ .withNetworkAliases("redis");
+ }
+
+ @Bean
+ @ServiceConnection
+ public DaprContainer daprContainer(Network daprNetwork, RedisContainer redisContainer, Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ Map redisProps = new HashMap<>();
+ redisProps.put("redisHost", "redis:6379");
+ redisProps.put("redisPassword", "");
+ redisProps.put("actorStateStore", String.valueOf(true));
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("worker-two")
+ .withNetwork(daprNetwork)
+ .withReusablePlacement(reuse)
+ .withReusableScheduler(reuse)
+ .withComponent(new Component("kvstore", "state.redis", "v1", redisProps))
+ .withAppPort(8082)
+ .withAppHealthCheckPath("/actuator/health")
+ .withAppChannelAddress("host.testcontainers.internal")
+ .dependsOn(redisContainer);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/TestWorkerTwoApplication.java b/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/TestWorkerTwoApplication.java
new file mode 100644
index 0000000000..8fbdce15e1
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/TestWorkerTwoApplication.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.workertwo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class TestWorkerTwoApplication {
+
+ public static void main(String[] args) {
+
+ SpringApplication.from(WorkerTwoApplication::main)
+ .with(DaprTestContainersConfig.class)
+ .run(args);
+ org.testcontainers.Testcontainers.exposeHostPorts(8082);
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/WorkerTwoAppIT.java b/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/WorkerTwoAppIT.java
new file mode 100644
index 0000000000..ed17115e8b
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/test/java/io/dapr/springboot/examples/workertwo/WorkerTwoAppIT.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.workertwo;
+
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest(classes = {TestWorkerTwoApplication.class, DaprTestContainersConfig.class},
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+class WorkerTwoAppIT {
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.baseURI = "http://localhost:" + 8082;
+ org.testcontainers.Testcontainers.exposeHostPorts(8082);
+
+ }
+
+ @Test
+ void testWorkerTwo() {
+ //Test the logic of the worker two
+ }
+
+}
diff --git a/spring-boot-examples/workflows/multi-app/worker-two/src/test/resources/application.properties b/spring-boot-examples/workflows/multi-app/worker-two/src/test/resources/application.properties
new file mode 100644
index 0000000000..2536d68631
--- /dev/null
+++ b/spring-boot-examples/workflows/multi-app/worker-two/src/test/resources/application.properties
@@ -0,0 +1,2 @@
+dapr.statestore.name=kvstore
+server.port=8082
diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/patterns/README.md
similarity index 99%
rename from spring-boot-examples/workflows/README.md
rename to spring-boot-examples/workflows/patterns/README.md
index d62eded716..b92294bdf7 100644
--- a/spring-boot-examples/workflows/README.md
+++ b/spring-boot-examples/workflows/patterns/README.md
@@ -29,7 +29,7 @@ timeout_seconds: 180
```sh
-../../mvnw spring-boot:test-run
+../../../mvnw spring-boot:test-run
```
diff --git a/spring-boot-examples/workflows/body.json b/spring-boot-examples/workflows/patterns/body.json
similarity index 100%
rename from spring-boot-examples/workflows/body.json
rename to spring-boot-examples/workflows/patterns/body.json
diff --git a/spring-boot-examples/workflows/patterns/pom.xml b/spring-boot-examples/workflows/patterns/pom.xml
new file mode 100644
index 0000000000..e4742686bc
--- /dev/null
+++ b/spring-boot-examples/workflows/patterns/pom.xml
@@ -0,0 +1,74 @@
+
+
+ 4.0.0
+
+
+ io.dapr
+ workflows
+ 1.17.0-SNAPSHOT
+
+
+ patterns
+ patterns
+ Spring Boot, Testcontainers and Dapr Integration Examples :: Workflows Patterns
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter
+
+
+ io.dapr.spring
+ dapr-spring-boot-starter-test
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ io.github.microcks
+ microcks-testcontainers
+ ${microcks.version}
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ true
+
+
+
+
+
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/DurationTimerWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/LogActivity.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/TimerLogService.java
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java
similarity index 100%
rename from spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java
rename to spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/timer/ZonedDateTimeTimerWorkflow.java
diff --git a/spring-boot-examples/workflows/src/main/resources/application.properties b/spring-boot-examples/workflows/patterns/src/main/resources/application.properties
similarity index 100%
rename from spring-boot-examples/workflows/src/main/resources/application.properties
rename to spring-boot-examples/workflows/patterns/src/main/resources/application.properties
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
similarity index 100%
rename from spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
rename to spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java b/spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java
similarity index 100%
rename from spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java
rename to spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppIT.java
similarity index 99%
rename from spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
rename to spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppIT.java
index 80a9cda01a..40ef080f5c 100644
--- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
+++ b/spring-boot-examples/workflows/patterns/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppIT.java
@@ -58,7 +58,7 @@
@SpringBootTest(classes = {TestWorkflowPatternsApplication.class, DaprTestContainersConfig.class,
DaprAutoConfiguration.class, },
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
-class WorkflowPatternsAppTests {
+class WorkflowPatternsAppIT {
@Autowired
private MicrocksContainersEnsemble ensemble;
diff --git a/spring-boot-examples/workflows/src/test/resources/application.properties b/spring-boot-examples/workflows/patterns/src/test/resources/application.properties
similarity index 100%
rename from spring-boot-examples/workflows/src/test/resources/application.properties
rename to spring-boot-examples/workflows/patterns/src/test/resources/application.properties
diff --git a/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml b/spring-boot-examples/workflows/patterns/src/test/resources/third-parties/remote-http-service.yaml
similarity index 100%
rename from spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml
rename to spring-boot-examples/workflows/patterns/src/test/resources/third-parties/remote-http-service.yaml
diff --git a/spring-boot-examples/workflows/pom.xml b/spring-boot-examples/workflows/pom.xml
index 0411a3e129..db4dc6f485 100644
--- a/spring-boot-examples/workflows/pom.xml
+++ b/spring-boot-examples/workflows/pom.xml
@@ -1,8 +1,8 @@
-
+
4.0.0
-
io.dapr
spring-boot-examples
@@ -10,50 +10,32 @@
workflows
- workflows
- Spring Boot, Testcontainers and Dapr Integration Examples :: Workflows
+ 1.17.0-SNAPSHOT
+ pom
+
+
+ true
+
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-test
-
-
- io.dapr.spring
- dapr-spring-boot-starter
-
-
- io.dapr.spring
- dapr-spring-boot-starter-test
- test
-
-
- io.rest-assured
- rest-assured
- test
-
-
- io.github.microcks
- microcks-testcontainers
- ${microcks.version}
- test
-
-
+
+ patterns
+ multi-app
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${springboot.version}
+ pom
+ import
+
+
+
-
- org.springframework.boot
- spring-boot-maven-plugin
-
org.apache.maven.plugins
maven-site-plugin
diff --git a/spring-boot-examples/workflows/spotbugs-exclude.xml b/spring-boot-examples/workflows/spotbugs-exclude.xml
new file mode 100644
index 0000000000..264fc79b0a
--- /dev/null
+++ b/spring-boot-examples/workflows/spotbugs-exclude.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+