Skip to content

Commit 3c45a18

Browse files
committed
Move cancel and reviseOrder saga orchestration to OrderSagaService
This removes saga dependencies from OrderServiceImpl, breaking the circular dependency between order-service-main and order-service-sagas modules. Changes: - Add cancel() and reviseOrder() methods to OrderSagaService - Remove saga-starting logic from OrderServiceImpl - Update OrderController to use OrderSagaService for cancel/revise - Remove cancel/reviseOrder from OrderService interface - Add OrderSagaServiceTest with tests for new methods Co-authored by Claude Code
1 parent 191c7ea commit 3c45a18

7 files changed

Lines changed: 130 additions & 81 deletions

File tree

ftgo-order-service/order-service-domain/src/main/java/net/chrisrichardson/ftgo/orderservice/domain/OrderService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ Order createOrder(long consumerId, long restaurantId, DeliveryInformation delive
1515

1616
Optional<Order> findById(long orderId);
1717

18-
Order cancel(Long orderId);
19-
20-
Order reviseOrder(long orderId, OrderRevision orderRevision);
21-
2218
// Saga command handler methods
2319
void approveOrder(long orderId);
2420

ftgo-order-service/order-service-main/src/main/java/net/chrisrichardson/ftgo/orderservice/domain/OrderServiceConfiguration.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import io.eventuate.tram.events.publisher.DomainEventPublisher;
44
import io.eventuate.tram.spring.events.publisher.TramEventsPublisherConfiguration;
55
import io.eventuate.tram.spring.flyway.EventuateTramFlywayMigrationConfiguration;
6-
import io.eventuate.tram.sagas.orchestration.*;
6+
import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory;
77
import io.eventuate.tram.sagas.spring.orchestration.SagaOrchestratorConfiguration;
88
import io.micrometer.core.instrument.MeterRegistry;
99
import net.chrisrichardson.ftgo.common.CommonConfiguration;
@@ -20,33 +20,29 @@
2020
import org.springframework.context.annotation.Configuration;
2121
import org.springframework.context.annotation.Import;
2222

23-
import org.springframework.context.annotation.Lazy;
24-
2523
import java.util.Optional;
2624

2725
@Configuration
2826
@Import({TramEventsPublisherConfiguration.class, SagaOrchestratorConfiguration.class, CommonConfiguration.class, EventuateTramFlywayMigrationConfiguration.class})
2927
public class OrderServiceConfiguration {
3028

3129
@Bean
32-
public OrderService orderService(@Lazy SagaInstanceFactory sagaInstanceFactory,
33-
RestaurantRepository restaurantRepository,
30+
public OrderService orderService(RestaurantRepository restaurantRepository,
3431
OrderRepository orderRepository,
35-
DomainEventPublisher eventPublisher,
36-
CancelOrderSaga cancelOrderSaga,
37-
ReviseOrderSaga reviseOrderSaga,
3832
OrderDomainEventPublisher orderAggregateEventPublisher,
3933
Optional<MeterRegistry> meterRegistry) {
4034

41-
return new OrderServiceImpl(sagaInstanceFactory, orderRepository, eventPublisher, restaurantRepository,
42-
cancelOrderSaga, reviseOrderSaga, orderAggregateEventPublisher, meterRegistry);
35+
return new OrderServiceImpl(orderRepository, restaurantRepository,
36+
orderAggregateEventPublisher, meterRegistry);
4337
}
4438

4539
@Bean
4640
public OrderSagaService orderSagaService(OrderRepository orderRepository,
4741
SagaInstanceFactory sagaInstanceFactory,
48-
CreateOrderSaga createOrderSaga) {
49-
return new OrderSagaService(orderRepository, sagaInstanceFactory, createOrderSaga);
42+
CreateOrderSaga createOrderSaga,
43+
CancelOrderSaga cancelOrderSaga,
44+
ReviseOrderSaga reviseOrderSaga) {
45+
return new OrderSagaService(orderRepository, sagaInstanceFactory, createOrderSaga, cancelOrderSaga, reviseOrderSaga);
5046
}
5147

5248
@Bean

ftgo-order-service/order-service-main/src/main/java/net/chrisrichardson/ftgo/orderservice/domain/OrderServiceImpl.java

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
package net.chrisrichardson.ftgo.orderservice.domain;
22

33
import io.eventuate.tram.events.aggregates.ResultWithDomainEvents;
4-
import io.eventuate.tram.events.publisher.DomainEventPublisher;
5-
import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory;
64
import io.micrometer.core.instrument.MeterRegistry;
75
import net.chrisrichardson.ftgo.orderservice.api.events.OrderDetails;
86
import net.chrisrichardson.ftgo.orderservice.api.events.OrderDomainEvent;
97
import net.chrisrichardson.ftgo.orderservice.api.events.OrderLineItem;
10-
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSaga;
11-
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSagaData;
12-
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSaga;
13-
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSagaData;
148
import org.slf4j.Logger;
159
import org.slf4j.LoggerFactory;
1610
import org.springframework.transaction.annotation.Transactional;
@@ -25,34 +19,20 @@ public class OrderServiceImpl implements OrderService {
2519

2620
private Logger logger = LoggerFactory.getLogger(getClass());
2721

28-
private SagaInstanceFactory sagaInstanceFactory;
29-
3022
private OrderRepository orderRepository;
3123

3224
private RestaurantRepository restaurantRepository;
3325

34-
private CancelOrderSaga cancelOrderSaga;
35-
36-
private ReviseOrderSaga reviseOrderSaga;
37-
3826
private OrderDomainEventPublisher orderAggregateEventPublisher;
3927

4028
private Optional<MeterRegistry> meterRegistry;
4129

42-
public OrderServiceImpl(SagaInstanceFactory sagaInstanceFactory,
43-
OrderRepository orderRepository,
44-
DomainEventPublisher eventPublisher,
30+
public OrderServiceImpl(OrderRepository orderRepository,
4531
RestaurantRepository restaurantRepository,
46-
CancelOrderSaga cancelOrderSaga,
47-
ReviseOrderSaga reviseOrderSaga,
4832
OrderDomainEventPublisher orderAggregateEventPublisher,
4933
Optional<MeterRegistry> meterRegistry) {
50-
51-
this.sagaInstanceFactory = sagaInstanceFactory;
5234
this.orderRepository = orderRepository;
5335
this.restaurantRepository = restaurantRepository;
54-
this.cancelOrderSaga = cancelOrderSaga;
55-
this.reviseOrderSaga = reviseOrderSaga;
5636
this.orderAggregateEventPublisher = orderAggregateEventPublisher;
5737
this.meterRegistry = meterRegistry;
5838
}
@@ -108,17 +88,6 @@ public void noteReversingAuthorization(Long orderId) {
10888
throw new UnsupportedOperationException();
10989
}
11090

111-
@Transactional
112-
public Order cancel(Long orderId) {
113-
Order order = orderRepository.findById(orderId)
114-
.orElseThrow(() -> new OrderNotFoundException(orderId));
115-
CancelOrderSagaData sagaData = new CancelOrderSagaData(order.getConsumerId(), orderId, order.getOrderTotal());
116-
sagaInstanceFactory.create(cancelOrderSaga, sagaData);
117-
// Access lazy-loaded collections within transaction to avoid LazyInitializationException
118-
order.getOrderTotal();
119-
return order;
120-
}
121-
12291
private Order updateOrder(long orderId, Function<Order, List<OrderDomainEvent>> updater) {
12392
return orderRepository.findById(orderId).map(order -> {
12493
orderAggregateEventPublisher.publish(order, updater.apply(order));
@@ -148,16 +117,6 @@ public void confirmCancelled(long orderId) {
148117
updateOrder(orderId, Order::noteCancelled);
149118
}
150119

151-
@Transactional
152-
public Order reviseOrder(long orderId, OrderRevision orderRevision) {
153-
Order order = orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException(orderId));
154-
ReviseOrderSagaData sagaData = new ReviseOrderSagaData(order.getConsumerId(), orderId, null, orderRevision);
155-
sagaInstanceFactory.create(reviseOrderSaga, sagaData);
156-
// Access lazy-loaded collections within transaction to avoid LazyInitializationException
157-
order.getOrderTotal();
158-
return order;
159-
}
160-
161120
public Optional<RevisedOrder> beginReviseOrder(long orderId, OrderRevision revision) {
162121
return orderRepository.findById(orderId).map(order -> {
163122
ResultWithDomainEvents<LineItemQuantityChange, OrderDomainEvent> result = order.revise(revision);
Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package net.chrisrichardson.ftgo.orderservice.domain;
22

3-
import io.eventuate.tram.events.publisher.DomainEventPublisher;
4-
import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory;
53
import net.chrisrichardson.ftgo.orderservice.OrderDetailsMother;
64
import net.chrisrichardson.ftgo.orderservice.RestaurantMother;
75
import net.chrisrichardson.ftgo.orderservice.api.events.OrderCreatedEvent;
8-
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSaga;
9-
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSaga;
106
import org.junit.jupiter.api.BeforeEach;
117
import org.junit.jupiter.api.Test;
128

@@ -23,38 +19,28 @@
2319
import static org.mockito.Mockito.any;
2420
import static org.mockito.Mockito.mock;
2521
import static org.mockito.Mockito.verify;
26-
import static org.mockito.Mockito.verifyNoInteractions;
2722
import static org.mockito.Mockito.when;
2823

2924
public class OrderServiceTest {
3025

3126
private OrderService orderService;
3227
private OrderRepository orderRepository;
33-
private DomainEventPublisher eventPublisher;
3428
private RestaurantRepository restaurantRepository;
35-
private SagaInstanceFactory sagaInstanceFactory;
36-
private CancelOrderSaga cancelOrderSaga;
37-
private ReviseOrderSaga reviseOrderSaga;
3829
private OrderDomainEventPublisher orderAggregateEventPublisher;
3930

4031
@BeforeEach
4132
public void setup() {
42-
sagaInstanceFactory = mock(SagaInstanceFactory.class);
4333
orderRepository = mock(OrderRepository.class);
44-
eventPublisher = mock(DomainEventPublisher.class);
4534
restaurantRepository = mock(RestaurantRepository.class);
46-
cancelOrderSaga = mock(CancelOrderSaga.class);
47-
reviseOrderSaga = mock(ReviseOrderSaga.class);
48-
4935
orderAggregateEventPublisher = mock(OrderDomainEventPublisher.class);
5036

51-
orderService = new OrderServiceImpl(sagaInstanceFactory, orderRepository, eventPublisher, restaurantRepository,
52-
cancelOrderSaga, reviseOrderSaga, orderAggregateEventPublisher, Optional.empty());
37+
orderService = new OrderServiceImpl(orderRepository, restaurantRepository,
38+
orderAggregateEventPublisher, Optional.empty());
5339
}
5440

5541

5642
@Test
57-
public void shouldCreateOrderWithoutStartingSaga() {
43+
public void shouldCreateOrder() {
5844
when(restaurantRepository.findById(AJANTA_ID)).thenReturn(Optional.of(AJANTA_RESTAURANT));
5945
when(orderRepository.save(any(Order.class))).then(invocation -> {
6046
Order order = (Order) invocation.getArguments()[0];
@@ -68,9 +54,6 @@ public void shouldCreateOrderWithoutStartingSaga() {
6854

6955
verify(orderAggregateEventPublisher).publish(order,
7056
Collections.singletonList(new OrderCreatedEvent(CHICKEN_VINDALOO_ORDER_DETAILS, OrderDetailsMother.DELIVERY_ADDRESS, RestaurantMother.AJANTA_RESTAURANT_NAME)));
71-
72-
// Verify saga is NOT started - saga orchestration is now handled by OrderSagaService
73-
verifyNoInteractions(sagaInstanceFactory);
7457
}
7558

7659
}

ftgo-order-service/order-service-restapi/src/main/java/net/chrisrichardson/ftgo/orderservice/web/OrderController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ private GetOrderResponse makeGetOrderResponse(Order order) {
5858
@RequestMapping(path = "/{orderId}/cancel", method = RequestMethod.POST)
5959
public ResponseEntity<GetOrderResponse> cancel(@PathVariable long orderId) {
6060
try {
61-
Order order = orderService.cancel(orderId);
61+
Order order = orderSagaService.cancel(orderId);
6262
return new ResponseEntity<>(makeGetOrderResponse(order), HttpStatus.OK);
6363
} catch (OrderNotFoundException e) {
6464
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -68,7 +68,7 @@ public ResponseEntity<GetOrderResponse> cancel(@PathVariable long orderId) {
6868
@RequestMapping(path = "/{orderId}/revise", method = RequestMethod.POST)
6969
public ResponseEntity<GetOrderResponse> revise(@PathVariable long orderId, @RequestBody ReviseOrderRequest request) {
7070
try {
71-
Order order = orderService.reviseOrder(orderId, new OrderRevision(Optional.empty(), request.getRevisedOrderLineItems()));
71+
Order order = orderSagaService.reviseOrder(orderId, new OrderRevision(Optional.empty(), request.getRevisedOrderLineItems()));
7272
return new ResponseEntity<>(makeGetOrderResponse(order), HttpStatus.OK);
7373
} catch (OrderNotFoundException e) {
7474
return new ResponseEntity<>(HttpStatus.NOT_FOUND);

ftgo-order-service/order-service-sagas/src/main/java/net/chrisrichardson/ftgo/orderservice/sagas/createorder/OrderSagaService.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
import net.chrisrichardson.ftgo.orderservice.domain.DeliveryInformation;
55
import net.chrisrichardson.ftgo.orderservice.domain.MenuItemIdAndQuantity;
66
import net.chrisrichardson.ftgo.orderservice.domain.Order;
7+
import net.chrisrichardson.ftgo.orderservice.domain.OrderNotFoundException;
78
import net.chrisrichardson.ftgo.orderservice.domain.OrderRepository;
9+
import net.chrisrichardson.ftgo.orderservice.domain.OrderRevision;
10+
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSaga;
11+
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSagaData;
12+
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSaga;
13+
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSagaData;
814
import org.springframework.transaction.annotation.Transactional;
915

1016
import java.util.List;
@@ -15,13 +21,19 @@ public class OrderSagaService {
1521
private final OrderRepository orderRepository;
1622
private final SagaInstanceFactory sagaInstanceFactory;
1723
private final CreateOrderSaga createOrderSaga;
24+
private final CancelOrderSaga cancelOrderSaga;
25+
private final ReviseOrderSaga reviseOrderSaga;
1826

1927
public OrderSagaService(OrderRepository orderRepository,
2028
SagaInstanceFactory sagaInstanceFactory,
21-
CreateOrderSaga createOrderSaga) {
29+
CreateOrderSaga createOrderSaga,
30+
CancelOrderSaga cancelOrderSaga,
31+
ReviseOrderSaga reviseOrderSaga) {
2232
this.orderRepository = orderRepository;
2333
this.sagaInstanceFactory = sagaInstanceFactory;
2434
this.createOrderSaga = createOrderSaga;
35+
this.cancelOrderSaga = cancelOrderSaga;
36+
this.reviseOrderSaga = reviseOrderSaga;
2537
}
2638

2739
public Order createOrder(long consumerId, long restaurantId,
@@ -31,4 +43,20 @@ public Order createOrder(long consumerId, long restaurantId,
3143
sagaInstanceFactory.create(createOrderSaga, data);
3244
return orderRepository.findById(data.getOrderId()).get();
3345
}
46+
47+
public Order cancel(long orderId) {
48+
Order order = orderRepository.findById(orderId)
49+
.orElseThrow(() -> new OrderNotFoundException(orderId));
50+
CancelOrderSagaData sagaData = new CancelOrderSagaData(order.getConsumerId(), orderId, order.getOrderTotal());
51+
sagaInstanceFactory.create(cancelOrderSaga, sagaData);
52+
return order;
53+
}
54+
55+
public Order reviseOrder(long orderId, OrderRevision orderRevision) {
56+
Order order = orderRepository.findById(orderId)
57+
.orElseThrow(() -> new OrderNotFoundException(orderId));
58+
ReviseOrderSagaData sagaData = new ReviseOrderSagaData(order.getConsumerId(), orderId, null, orderRevision);
59+
sagaInstanceFactory.create(reviseOrderSaga, sagaData);
60+
return order;
61+
}
3462
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package net.chrisrichardson.ftgo.orderservice.sagas.createorder;
2+
3+
import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory;
4+
import net.chrisrichardson.ftgo.common.Money;
5+
import net.chrisrichardson.ftgo.orderservice.domain.Order;
6+
import net.chrisrichardson.ftgo.orderservice.domain.OrderRepository;
7+
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSaga;
8+
import net.chrisrichardson.ftgo.orderservice.sagas.cancelorder.CancelOrderSagaData;
9+
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSaga;
10+
import net.chrisrichardson.ftgo.orderservice.sagas.reviseorder.ReviseOrderSagaData;
11+
import net.chrisrichardson.ftgo.orderservice.domain.OrderRevision;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
import org.mockito.ArgumentCaptor;
15+
16+
import java.util.Optional;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.mockito.ArgumentMatchers.same;
20+
import static org.mockito.Mockito.*;
21+
22+
public class OrderSagaServiceTest {
23+
24+
private OrderRepository orderRepository;
25+
private SagaInstanceFactory sagaInstanceFactory;
26+
private CreateOrderSaga createOrderSaga;
27+
private CancelOrderSaga cancelOrderSaga;
28+
private ReviseOrderSaga reviseOrderSaga;
29+
private OrderSagaService orderSagaService;
30+
31+
private static final long ORDER_ID = 99L;
32+
private static final long CONSUMER_ID = 1L;
33+
private static final Money ORDER_TOTAL = new Money("100.00");
34+
35+
@BeforeEach
36+
public void setUp() {
37+
orderRepository = mock(OrderRepository.class);
38+
sagaInstanceFactory = mock(SagaInstanceFactory.class);
39+
createOrderSaga = mock(CreateOrderSaga.class);
40+
cancelOrderSaga = mock(CancelOrderSaga.class);
41+
reviseOrderSaga = mock(ReviseOrderSaga.class);
42+
orderSagaService = new OrderSagaService(orderRepository, sagaInstanceFactory, createOrderSaga, cancelOrderSaga, reviseOrderSaga);
43+
}
44+
45+
@Test
46+
public void shouldCancelOrder() {
47+
Order order = mock(Order.class);
48+
when(order.getId()).thenReturn(ORDER_ID);
49+
when(order.getConsumerId()).thenReturn(CONSUMER_ID);
50+
when(order.getOrderTotal()).thenReturn(ORDER_TOTAL);
51+
when(orderRepository.findById(ORDER_ID)).thenReturn(Optional.of(order));
52+
53+
Order result = orderSagaService.cancel(ORDER_ID);
54+
55+
assertEquals(order, result);
56+
57+
ArgumentCaptor<CancelOrderSagaData> sagaDataCaptor = ArgumentCaptor.forClass(CancelOrderSagaData.class);
58+
verify(sagaInstanceFactory).create(same(cancelOrderSaga), sagaDataCaptor.capture());
59+
60+
CancelOrderSagaData sagaData = sagaDataCaptor.getValue();
61+
assertEquals(ORDER_ID, sagaData.getOrderId());
62+
assertEquals(CONSUMER_ID, sagaData.getConsumerId());
63+
assertEquals(ORDER_TOTAL, sagaData.getOrderTotal());
64+
}
65+
66+
@Test
67+
public void shouldReviseOrder() {
68+
Order order = mock(Order.class);
69+
when(order.getId()).thenReturn(ORDER_ID);
70+
when(order.getConsumerId()).thenReturn(CONSUMER_ID);
71+
when(orderRepository.findById(ORDER_ID)).thenReturn(Optional.of(order));
72+
73+
OrderRevision orderRevision = mock(OrderRevision.class);
74+
75+
Order result = orderSagaService.reviseOrder(ORDER_ID, orderRevision);
76+
77+
assertEquals(order, result);
78+
79+
ArgumentCaptor<ReviseOrderSagaData> sagaDataCaptor = ArgumentCaptor.forClass(ReviseOrderSagaData.class);
80+
verify(sagaInstanceFactory).create(same(reviseOrderSaga), sagaDataCaptor.capture());
81+
82+
ReviseOrderSagaData sagaData = sagaDataCaptor.getValue();
83+
assertEquals(ORDER_ID, sagaData.getOrderId());
84+
assertEquals(CONSUMER_ID, sagaData.getConsumerId());
85+
assertEquals(orderRevision, sagaData.getOrderRevision());
86+
}
87+
}

0 commit comments

Comments
 (0)