Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ jobs:
(cd examples && mm.py ./src/main/java/io/dapr/examples/workflows/README.md)
(cd spring-boot-examples/pubsub && mm.py README.md)
(cd spring-boot-examples/workflows/patterns && mm.py README.md)
(cd spring-boot-examples/workflows/event-workflow && mm.py README.md)
(cd examples && mm.py ./src/main/java/io/dapr/examples/jobs/README.md)
(cd examples && mm.py ./src/main/java/io/dapr/examples/conversation/README.md)
(cd examples && mm.py ./src/main/java/io/dapr/examples/invoke/http/README.md)
Expand Down
1 change: 1 addition & 0 deletions spring-boot-examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ folder per Dapr API:
- [PubSub](pubsub/README.md): a producer app publishing messages with Spring Messaging and a consumer app subscribing to them.
- Workflows:
- [Patterns](workflows/patterns/README.md): common Dapr Workflow patterns.
- [Event Workflow](workflows/event-workflow/README.md): workflows started and continued by Pub/Sub events.
- [Multi App](workflows/multi-app/README.md): workflows that span multiple applications.
- [Versioning](workflows/versioning/README.md): versioning workflows.
182 changes: 182 additions & 0 deletions spring-boot-examples/workflows/event-workflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Dapr Spring Boot Event Driven Workflow Example

This application shows how to combine [Dapr Workflows](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/)
with [Dapr Pub/Sub](https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/) in a Spring Boot
application:

- An `orders` event starts a new workflow instance.
- The workflow waits for a `payments` event, which is forwarded to it as an external event.
- The workflow publishes its progress to the `order-status` topic from an activity.

```mermaid
graph LR
O[orders topic] -->|start workflow| SW((Start
Workflow))
SW --> A1[Reserve Inventory]
A1 --> A2[Publish WAITING_FOR_PAYMENT]
A2 --> E{Wait for
PaymentReceived}
P[payments topic] -->|raise event| E
E --> A3[Publish COMPLETED]
A3 --> EW((End
Workflow))
A2 --> S[order-status topic]
A3 --> S
```

The main classes are:

- [`OrderEventsSubscriber`](src/main/java/io/dapr/springboot/examples/eventworkflow/OrderEventsSubscriber.java):
subscribes to the topics using the `@Topic` annotation. It starts the workflow with `DaprWorkflowClient.scheduleNewWorkflow`,
using the order id as the workflow instance id, and forwards payments with `DaprWorkflowClient.raiseEvent`.
- [`OrderProcessingWorkflow`](src/main/java/io/dapr/springboot/examples/eventworkflow/OrderProcessingWorkflow.java):
waits for the `PaymentReceived` external event. If the payment does not arrive within 5 minutes, the order is cancelled.
- [`PublishOrderStatusActivity`](src/main/java/io/dapr/springboot/examples/eventworkflow/PublishOrderStatusActivity.java):
publishes the status events using `DaprMessagingTemplate`. Publishing happens in an activity because workflow code
is replayed and must not perform I/O directly.

## Running this example from source code

To run this example you will need:
- Java SDK
- Maven
- Docker or a container runtime such as Podman

From the `spring-boot-examples/workflows/event-workflow` directory you can start the service by running the following command:

<!-- STEP
name: Run Event Workflow Application
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Started EventWorkflowApplication'
background: true
expected_return_code: 143
sleep: 60
timeout_seconds: 240
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
../../../mvnw spring-boot:test-run
```

<!-- END_STEP -->

By running the `spring-boot:test-run` goal, the application is loaded using the [test configurations](src/test/java/io/dapr/springboot/examples/eventworkflow/DaprTestContainersConfig.java)
configured using [Testcontainers](https://testcontainers.com) to bootstrap the [Dapr](https://dapr.io) sidecar,
PostgreSQL (used as the workflow state store) and RabbitMQ (used as the Pub/Sub broker).

## Placing an order

Publish an order event by sending a POST request to the application:

<!-- STEP
name: Place an order
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Order Event Published: order-1'
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl -X POST localhost:8080/orders -H 'Content-Type: application/json' -d '{ "id": "order-1", "item": "the mars volta EP", "amount": 1 }' --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

The subscriber of the `orders` topic starts a workflow instance. The workflow reserves the inventory and publishes
the `WAITING_FOR_PAYMENT` status. You can check the last status received from the `order-status` topic:

<!-- STEP
name: Check order is waiting for payment
match_order: none
output_match_mode: substring
expected_stdout_lines:
- '"status":"WAITING_FOR_PAYMENT"'
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl --fail localhost:8080/orders/order-1/status --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

```bash
{"orderId":"order-1","status":"WAITING_FOR_PAYMENT"}
```

In the application output you should see:

```bash
i.d.s.e.e.OrdersRestController : Publishing Order Event: Order{id='order-1', item='the mars volta EP', amount=1}
i.d.s.e.e.OrderEventsSubscriber : Order Event Received: Order{id='order-1', item='the mars volta EP', amount=1}
i.d.s.e.e.OrderEventsSubscriber : Workflow instance order-1 started
i.d.s.e.e.OrderProcessingWorkflow : Starting Workflow: io.dapr.springboot.examples.eventworkflow.OrderProcessingWorkflow
i.d.s.e.e.ReserveInventoryActivity : Reserving 1 unit(s) of 'the mars volta EP' for order order-1
i.d.s.e.e.PublishOrderStatusActivity : Publishing Order Status Event: OrderStatus{orderId='order-1', status='WAITING_FOR_PAYMENT'}
i.d.s.e.e.OrderEventsSubscriber : Order Status Event Received: OrderStatus{orderId='order-1', status='WAITING_FOR_PAYMENT'}
```

## Paying for the order

Publish a payment event for the order:

<!-- STEP
name: Pay the order
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Payment Event Published: order-1'
sleep: 10
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl -X POST localhost:8080/orders/order-1/payment -H 'Content-Type: application/json' -d '{ "total": 10.0 }' --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

The subscriber of the `payments` topic raises the `PaymentReceived` event on the workflow instance `order-1`, and the
workflow completes the order:

<!-- STEP
name: Check order is completed
match_order: none
output_match_mode: substring
expected_stdout_lines:
- '"status":"COMPLETED"'
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl --fail localhost:8080/orders/order-1/status --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

```bash
{"orderId":"order-1","status":"COMPLETED"}
```

In the application output you should see:

```bash
i.d.s.e.e.OrderEventsSubscriber : Payment Event Received: Payment{orderId='order-1', total=10.0}
i.d.s.e.e.OrderProcessingWorkflow : Payment received for order order-1: Payment{orderId='order-1', total=10.0}
i.d.s.e.e.PublishOrderStatusActivity : Publishing Order Status Event: OrderStatus{orderId='order-1', status='COMPLETED'}
i.d.s.e.e.OrderEventsSubscriber : Order Status Event Received: OrderStatus{orderId='order-1', status='COMPLETED'}
```

## Testing event driven workflows

The [`EventWorkflowAppIT`](src/test/java/io/dapr/springboot/examples/eventworkflow/EventWorkflowAppIT.java) test uses
Testcontainers to run the same flow end to end: it places an order, waits for the `WAITING_FOR_PAYMENT` status,
pays the order and checks that the workflow instance completed.
75 changes: 75 additions & 0 deletions spring-boot-examples/workflows/event-workflow/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.dapr</groupId>
<artifactId>workflows</artifactId>
<version>1.20.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>event-workflow</artifactId>
<name>event-workflow</name>
<description>Spring Boot 4, Testcontainers and Dapr Integration Examples :: Event Driven Workflow</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-rabbitmq</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<!-- Skip checkstyle for auto-generated code -->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2026 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.eventworkflow;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EventWorkflowApplication {

public static void main(String[] args) {
SpringApplication.run(EventWorkflowApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2026 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.eventworkflow;

import io.dapr.client.DaprClient;
import io.dapr.spring.boot.properties.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 EventWorkflowConfiguration {

public static final String ORDERS_TOPIC = "orders";
public static final String PAYMENTS_TOPIC = "payments";
public static final String ORDER_STATUS_TOPIC = "order-status";

@Bean
public DaprMessagingTemplate<Order> orderMessagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
}

@Bean
public DaprMessagingTemplate<Payment> paymentMessagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
}

@Bean
public DaprMessagingTemplate<OrderStatus> orderStatusMessagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName(), false);
}
}
Loading
Loading