Skip to content
Draft
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 @@ -125,6 +125,7 @@ jobs:
(cd examples && mm.py ./src/main/java/io/dapr/examples/binarystore/README.md)
(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/simple-workflow && 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)
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 @@ -5,6 +5,7 @@ folder per Dapr API:

- [PubSub](pubsub/README.md): a producer app publishing messages with Spring Messaging and a consumer app subscribing to them.
- Workflows:
- [Simple Workflow](workflows/simple-workflow/README.md): a first Spring Boot workflow, started from a REST endpoint.
- [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.
Expand Down
1 change: 1 addition & 0 deletions spring-boot-examples/workflows/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</properties>

<modules>
<module>simple-workflow</module>
<module>patterns</module>
<module>multi-app</module>
<module>versioning</module>
Expand Down
165 changes: 165 additions & 0 deletions spring-boot-examples/workflows/simple-workflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Dapr Spring Boot Simple Workflow Example

This application is the starting point for using [Dapr Workflows](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/)
with Spring Boot. It onboards a customer in three steps:

```mermaid
graph LR
SW((Start
Workflow))
A1[Validate Customer]
A2[Create Account]
A3[Send Welcome Email]
EW((End
Workflow))
SW --> A1
A1 --> A2
A2 --> A3
A3 --> EW
```

It shows how to:

- Define a workflow ([`CustomerOnboardingWorkflow`](src/main/java/io/dapr/springboot/examples/simpleworkflow/CustomerOnboardingWorkflow.java))
and its activities as Spring `@Component`s, and register them with `@EnableDaprWorkflows`.
- Inject regular Spring beans into activities, like the [`AccountService`](src/main/java/io/dapr/springboot/examples/simpleworkflow/AccountService.java)
used by `CreateAccountActivity`.
- Start a workflow from a REST endpoint with `DaprWorkflowClient.scheduleNewWorkflow` without waiting for it to finish,
and check its status and result later with `DaprWorkflowClient.getWorkflowState`.
- See a workflow fail when one of its activities throws an exception.

For more workflow patterns, such as fan out/fan in, timers, child workflows or external events, see the
[workflow patterns example](../patterns/README.md).

## 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/simple-workflow` directory you can start the service by running the following command:

<!-- STEP
name: Run Simple Workflow Application
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'Started SimpleWorkflowApplication'
background: true
expected_return_code: 143
timeout_seconds: 180
-->
<!-- 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/simpleworkflow/DaprTestContainersConfig.java)
configured using [Testcontainers](https://testcontainers.com) to bootstrap the [Dapr](https://dapr.io) sidecar and
control plane. No Dapr components are configured, so the workflow state is kept in an in-memory state store.

## Onboarding a customer

Start the workflow by sending a POST request with the customer. The customer id is used as the workflow instance id,
and the request returns as soon as the workflow is scheduled:

<!-- STEP
name: Start onboarding workflow
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'customer-1'
sleep: 5
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl -X POST localhost:8080/onboarding -H 'Content-Type: application/json' -d '{ "id": "customer-1", "name": "Ada Lovelace", "email": "ada@example.com" }' --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

Then check the status of the workflow instance:

<!-- STEP
name: Check onboarding workflow
match_order: none
output_match_mode: substring
expected_stdout_lines:
- '"runtimeStatus":"COMPLETED"'
- '"accountId":"account-customer-1"'
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl --fail localhost:8080/onboarding/customer-1 --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

```bash
{"error":null,"instanceId":"customer-1","result":{"accountId":"account-customer-1","customerId":"customer-1","welcomeEmailSent":true},"runtimeStatus":"COMPLETED"}
```

In the application output you should see the activities being executed:

```bash
i.d.s.e.s.OnboardingRestController : Workflow instance customer-1 started
i.d.s.e.s.CustomerOnboardingWorkflow : Starting Workflow: io.dapr.springboot.examples.simpleworkflow.CustomerOnboardingWorkflow
i.d.s.e.s.ValidateCustomerActivity : Customer customer-1 is valid
i.d.s.e.s.CreateAccountActivity : Account account-customer-1 created for customer customer-1
i.d.s.e.s.SendWelcomeEmailActivity : Sending welcome email to ada@example.com
i.d.s.e.s.CustomerOnboardingWorkflow : Workflow finished with result: OnboardingResult{customerId='customer-1', accountId='account-customer-1', welcomeEmailSent=true}
```

## A failing workflow

If an activity throws an exception and the workflow does not catch it, the workflow instance fails. Start the
onboarding for a customer without an email:

<!-- STEP
name: Start onboarding workflow for invalid customer
match_order: none
output_match_mode: substring
expected_stdout_lines:
- 'customer-2'
sleep: 5
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl -X POST localhost:8080/onboarding -H 'Content-Type: application/json' -d '{ "id": "customer-2", "name": "No Email" }' --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

The status shows that the workflow failed, and why:

<!-- STEP
name: Check failed onboarding workflow
match_order: none
output_match_mode: substring
expected_stdout_lines:
- '"runtimeStatus":"FAILED"'
- 'Invalid email for customer customer-2'
timeout_seconds: 90
-->
<!-- Timeout for above service must be more than sleep + timeout for the client-->

```sh
curl --fail localhost:8080/onboarding/customer-2 --retry 10 --max-time 20 --retry-all-errors --retry-max-time 90
```

<!-- END_STEP -->

## Testing workflows

The [`SimpleWorkflowAppIT`](src/test/java/io/dapr/springboot/examples/simpleworkflow/SimpleWorkflowAppIT.java) test
uses Testcontainers to run both scenarios end to end.
65 changes: 65 additions & 0 deletions spring-boot-examples/workflows/simple-workflow/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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>simple-workflow</artifactId>
<name>simple-workflow</name>
<description>Spring Boot 4, Testcontainers and Dapr Integration Examples :: Simple 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>
</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,33 @@
/*
* 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.simpleworkflow;

import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Regular Spring service injected into the workflow activities.
* Creating an account twice for the same customer returns the same account, so activity retries are safe.
*/
@Service
public class AccountService {

private final Map<String, String> accounts = new ConcurrentHashMap<>();

public String createAccount(Customer customer) {
return accounts.computeIfAbsent(customer.getId(), id -> "account-" + id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.simpleworkflow;

import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CreateAccountActivity implements WorkflowActivity {

private final Logger logger = LoggerFactory.getLogger(CreateAccountActivity.class);

@Autowired
private AccountService accountService;

@Override
public Object run(WorkflowActivityContext ctx) {
logger.info("Starting Activity: " + ctx.getName());
Customer customer = ctx.getInput(Customer.class);
String accountId = accountService.createAccount(customer);
logger.info("Account " + accountId + " created for customer " + customer.getId());
return accountId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.simpleworkflow;

public class Customer {
private String id;
private String name;
private String email;

public Customer() {
}

/**
* Creates a new Customer.
*
* @param id customer id
* @param name customer name
* @param email customer email
*/
public Customer(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

@Override
public String toString() {
return "Customer{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", email='" + email + '\'' + '}';
}
}
Loading
Loading