Skip to content

Commit 6fde99e

Browse files
Siri Varma Vegirajuclaude
authored andcommitted
feat: Add compensation workflow pattern to Spring Boot examples
Port the BookTrip compensation (Saga) workflow from the plain Java examples into the Spring Boot workflow patterns module, adding @Component-annotated activities and a /wfp/compensation REST endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Siri Varma Vegiraju <svegiraju@Siris-MacBook-Pro.local>
1 parent f63a4a4 commit 6fde99e

File tree

8 files changed

+361
-0
lines changed

8 files changed

+361
-0
lines changed

spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
1717
import io.dapr.springboot.examples.wfp.chain.ChainWorkflow;
18+
import io.dapr.springboot.examples.wfp.compensation.BookTripWorkflow;
1819
import io.dapr.springboot.examples.wfp.child.ParentWorkflow;
1920
import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog;
2021
import io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow;
@@ -191,6 +192,19 @@ public Decision suspendResumeContinue(@RequestParam("orderId") String orderId, @
191192
return workflowInstanceStatus.readOutputAs(Decision.class);
192193
}
193194

195+
/**
196+
* Run Compensation Demo Workflow (Book Trip with Saga pattern).
197+
* @return the output of the BookTripWorkflow execution
198+
*/
199+
@PostMapping("wfp/compensation")
200+
public String compensation() throws TimeoutException {
201+
String instanceId = daprWorkflowClient.scheduleNewWorkflow(BookTripWorkflow.class);
202+
logger.info("Workflow instance " + instanceId + " started");
203+
return daprWorkflowClient
204+
.waitForWorkflowCompletion(instanceId, Duration.ofSeconds(30), true)
205+
.readOutputAs(String.class);
206+
}
207+
194208
@PostMapping("wfp/durationtimer")
195209
public String durationTimerWorkflow() {
196210
return daprWorkflowClient.scheduleNewWorkflow(DurationTimerWorkflow.class);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import java.util.concurrent.TimeUnit;
22+
23+
import org.springframework.stereotype.Component;
24+
25+
@Component
26+
public class BookCarActivity implements WorkflowActivity {
27+
private static final Logger logger = LoggerFactory.getLogger(BookCarActivity.class);
28+
29+
@Override
30+
public Object run(WorkflowActivityContext ctx) {
31+
logger.info("Starting Activity: " + ctx.getName());
32+
33+
try {
34+
TimeUnit.SECONDS.sleep(2);
35+
} catch (InterruptedException e) {
36+
throw new RuntimeException(e);
37+
}
38+
39+
logger.info("Forcing Failure to trigger compensation for activity: " + ctx.getName());
40+
throw new RuntimeException("Failed to book car");
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.stereotype.Component;
21+
22+
import java.util.concurrent.TimeUnit;
23+
24+
@Component
25+
public class BookFlightActivity implements WorkflowActivity {
26+
private static final Logger logger = LoggerFactory.getLogger(BookFlightActivity.class);
27+
28+
@Override
29+
public Object run(WorkflowActivityContext ctx) {
30+
logger.info("Starting Activity: " + ctx.getName());
31+
32+
try {
33+
TimeUnit.SECONDS.sleep(2);
34+
} catch (InterruptedException e) {
35+
throw new RuntimeException(e);
36+
}
37+
38+
String result = "Flight booked successfully";
39+
logger.info("Activity completed with result: " + result);
40+
return result;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.stereotype.Component;
21+
22+
@Component
23+
public class BookHotelActivity implements WorkflowActivity {
24+
private static final Logger logger = LoggerFactory.getLogger(BookHotelActivity.class);
25+
26+
@Override
27+
public Object run(WorkflowActivityContext ctx) {
28+
logger.info("Starting Activity: " + ctx.getName());
29+
30+
try {
31+
Thread.sleep(2000);
32+
} catch (InterruptedException e) {
33+
Thread.currentThread().interrupt();
34+
}
35+
36+
String result = "Hotel booked successfully";
37+
logger.info("Activity completed with result: " + result);
38+
return result;
39+
}
40+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.durabletask.TaskFailedException;
17+
import io.dapr.workflows.Workflow;
18+
import io.dapr.workflows.WorkflowStub;
19+
import io.dapr.workflows.WorkflowTaskOptions;
20+
import io.dapr.workflows.WorkflowTaskRetryPolicy;
21+
import org.springframework.stereotype.Component;
22+
23+
import java.time.Duration;
24+
import java.util.ArrayList;
25+
import java.util.Collections;
26+
import java.util.List;
27+
28+
@Component
29+
public class BookTripWorkflow implements Workflow {
30+
@Override
31+
public WorkflowStub create() {
32+
return ctx -> {
33+
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
34+
List<String> compensations = new ArrayList<>();
35+
36+
WorkflowTaskRetryPolicy compensationRetryPolicy = WorkflowTaskRetryPolicy.newBuilder()
37+
.setFirstRetryInterval(Duration.ofSeconds(1))
38+
.setMaxNumberOfAttempts(3)
39+
.build();
40+
41+
WorkflowTaskOptions compensationOptions = new WorkflowTaskOptions(compensationRetryPolicy);
42+
43+
try {
44+
String flightResult = ctx.callActivity(
45+
BookFlightActivity.class.getName(), null, String.class).await();
46+
ctx.getLogger().info("Flight booking completed: {}", flightResult);
47+
compensations.add("CancelFlight");
48+
49+
String hotelResult = ctx.callActivity(
50+
BookHotelActivity.class.getName(), null, String.class).await();
51+
ctx.getLogger().info("Hotel booking completed: {}", hotelResult);
52+
compensations.add("CancelHotel");
53+
54+
String carResult = ctx.callActivity(
55+
BookCarActivity.class.getName(), null, String.class).await();
56+
ctx.getLogger().info("Car booking completed: {}", carResult);
57+
compensations.add("CancelCar");
58+
59+
String result = String.format("%s, %s, %s", flightResult, hotelResult, carResult);
60+
ctx.getLogger().info("Trip booked successfully: {}", result);
61+
ctx.complete(result);
62+
63+
} catch (TaskFailedException e) {
64+
ctx.getLogger().info("******** executing compensation logic ********");
65+
ctx.getLogger().error("Activity failed: {}", e.getMessage());
66+
67+
Collections.reverse(compensations);
68+
for (String compensation : compensations) {
69+
try {
70+
switch (compensation) {
71+
case "CancelCar":
72+
String carCancelResult = ctx.callActivity(
73+
CancelCarActivity.class.getName(), null, compensationOptions, String.class).await();
74+
ctx.getLogger().info("Car cancellation completed: {}", carCancelResult);
75+
break;
76+
case "CancelHotel":
77+
String hotelCancelResult = ctx.callActivity(
78+
CancelHotelActivity.class.getName(), null, compensationOptions, String.class).await();
79+
ctx.getLogger().info("Hotel cancellation completed: {}", hotelCancelResult);
80+
break;
81+
case "CancelFlight":
82+
String flightCancelResult = ctx.callActivity(
83+
CancelFlightActivity.class.getName(), null, compensationOptions, String.class).await();
84+
ctx.getLogger().info("Flight cancellation completed: {}", flightCancelResult);
85+
break;
86+
default:
87+
break;
88+
}
89+
} catch (TaskFailedException ex) {
90+
ctx.getLogger().error("Activity failed during compensation: {}", ex.getMessage());
91+
}
92+
}
93+
ctx.complete("Workflow failed, compensation applied");
94+
}
95+
};
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.stereotype.Component;
21+
22+
import java.util.concurrent.TimeUnit;
23+
24+
@Component
25+
public class CancelCarActivity implements WorkflowActivity {
26+
private static final Logger logger = LoggerFactory.getLogger(CancelCarActivity.class);
27+
28+
@Override
29+
public Object run(WorkflowActivityContext ctx) {
30+
logger.info("Starting Activity: " + ctx.getName());
31+
32+
try {
33+
TimeUnit.SECONDS.sleep(2);
34+
} catch (InterruptedException e) {
35+
throw new RuntimeException(e);
36+
}
37+
38+
String result = "Car canceled successfully";
39+
logger.info("Activity completed with result: " + result);
40+
return result;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.stereotype.Component;
21+
22+
import java.util.concurrent.TimeUnit;
23+
24+
@Component
25+
public class CancelFlightActivity implements WorkflowActivity {
26+
private static final Logger logger = LoggerFactory.getLogger(CancelFlightActivity.class);
27+
28+
@Override
29+
public Object run(WorkflowActivityContext ctx) {
30+
logger.info("Starting Activity: " + ctx.getName());
31+
32+
try {
33+
TimeUnit.SECONDS.sleep(2);
34+
} catch (InterruptedException e) {
35+
throw new RuntimeException(e);
36+
}
37+
38+
String result = "Flight canceled successfully";
39+
logger.info("Activity completed with result: " + result);
40+
return result;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.springboot.examples.wfp.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.stereotype.Component;
21+
22+
import java.util.concurrent.TimeUnit;
23+
24+
@Component
25+
public class CancelHotelActivity implements WorkflowActivity {
26+
private static final Logger logger = LoggerFactory.getLogger(CancelHotelActivity.class);
27+
28+
@Override
29+
public Object run(WorkflowActivityContext ctx) {
30+
logger.info("Starting Activity: " + ctx.getName());
31+
32+
try {
33+
TimeUnit.SECONDS.sleep(2);
34+
} catch (InterruptedException e) {
35+
throw new RuntimeException(e);
36+
}
37+
38+
String result = "Hotel canceled successfully";
39+
logger.info("Activity completed with result: " + result);
40+
return result;
41+
}
42+
}

0 commit comments

Comments
 (0)