Skip to content

Commit a47b369

Browse files
committed
Publish Order Created Event using Outbox Pattern
Explain the challenge of implementing "database operations and sending a message" as a single atomic operation. Explain how "Outbox Pattern" tries to solve this problem. Create order_events table, Entity, Repository, Service Create order even models Implement Order Event Mapper Store Order events into order_events table Create a Scheduled Job to publish order events to RabbitMQ
1 parent b775f8f commit a47b369

14 files changed

+326
-56
lines changed

order-service/src/main/java/com/codebykavindu/bookstore/orders/OrderServiceApplication.java

+2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
6+
import org.springframework.scheduling.annotation.EnableScheduling;
67

78
@SpringBootApplication
89
@ConfigurationPropertiesScan
10+
@EnableScheduling
911
public class OrderServiceApplication {
1012

1113
public static void main(String[] args) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.codebykavindu.bookstore.orders.domain;
2+
3+
import com.codebykavindu.bookstore.orders.domain.models.OrderEventType;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.EnumType;
7+
import jakarta.persistence.Enumerated;
8+
import jakarta.persistence.GeneratedValue;
9+
import jakarta.persistence.GenerationType;
10+
import jakarta.persistence.Id;
11+
import jakarta.persistence.SequenceGenerator;
12+
import jakarta.persistence.Table;
13+
import java.time.LocalDateTime;
14+
15+
/**
16+
* @author Kavindu Perera
17+
*/
18+
@Entity
19+
@Table(name = "order_events")
20+
class OrderEventEntity {
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_event_id_generator")
23+
@SequenceGenerator(name = "order_event_id_generator", sequenceName = "order_event_id_seq")
24+
private Long id;
25+
26+
@Column(nullable = false)
27+
private String orderNumber;
28+
29+
@Column(nullable = false, unique = true)
30+
private String eventId;
31+
32+
@Enumerated(EnumType.STRING)
33+
private OrderEventType eventType;
34+
35+
@Column(nullable = false)
36+
private String payload;
37+
38+
@Column(name = "created_at", nullable = false, updatable = false)
39+
private LocalDateTime createdAt = LocalDateTime.now();
40+
41+
@Column(name = "updated_at")
42+
private LocalDateTime updatedAt;
43+
44+
public Long getId() {
45+
return id;
46+
}
47+
48+
public void setId(Long id) {
49+
this.id = id;
50+
}
51+
52+
public String getOrderNumber() {
53+
return orderNumber;
54+
}
55+
56+
public void setOrderNumber(String orderNumber) {
57+
this.orderNumber = orderNumber;
58+
}
59+
60+
public String getEventId() {
61+
return eventId;
62+
}
63+
64+
public void setEventId(String eventId) {
65+
this.eventId = eventId;
66+
}
67+
68+
public OrderEventType getEventType() {
69+
return eventType;
70+
}
71+
72+
public void setEventType(OrderEventType eventType) {
73+
this.eventType = eventType;
74+
}
75+
76+
public String getPayload() {
77+
return payload;
78+
}
79+
80+
public void setPayload(String payload) {
81+
this.payload = payload;
82+
}
83+
84+
public LocalDateTime getCreatedAt() {
85+
return createdAt;
86+
}
87+
88+
public void setCreatedAt(LocalDateTime createdAt) {
89+
this.createdAt = createdAt;
90+
}
91+
92+
public LocalDateTime getUpdatedAt() {
93+
return updatedAt;
94+
}
95+
96+
public void setUpdatedAt(LocalDateTime updatedAt) {
97+
this.updatedAt = updatedAt;
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.codebykavindu.bookstore.orders.domain;
2+
3+
import com.codebykavindu.bookstore.orders.domain.models.OrderCreatedEvent;
4+
import com.codebykavindu.bookstore.orders.domain.models.OrderItem;
5+
import java.time.LocalDateTime;
6+
import java.util.Set;
7+
import java.util.UUID;
8+
import java.util.stream.Collectors;
9+
10+
/**
11+
* @author Kavindu Perera
12+
*/
13+
class OrderEventMapper {
14+
private OrderEventMapper() {}
15+
16+
static OrderCreatedEvent buildOrderCreatedEvent(OrderEntity order) {
17+
return new OrderCreatedEvent(
18+
UUID.randomUUID().toString(),
19+
order.getOrderNumber(),
20+
getOrderItems(order),
21+
order.getCustomer(),
22+
order.getDeliveryAddress(),
23+
LocalDateTime.now());
24+
}
25+
26+
private static Set<OrderItem> getOrderItems(OrderEntity order) {
27+
return order.getItems().stream()
28+
.map(item -> new OrderItem(item.getCode(), item.getName(), item.getPrice(), item.getQuantity()))
29+
.collect(Collectors.toSet());
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.codebykavindu.bookstore.orders.domain;
2+
3+
import com.codebykavindu.bookstore.orders.ApplicationProperties;
4+
import com.codebykavindu.bookstore.orders.domain.models.OrderCreatedEvent;
5+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
6+
import org.springframework.stereotype.Component;
7+
8+
/**
9+
* @author Kavindu Perera
10+
*/
11+
@Component
12+
class OrderEventPublisher {
13+
14+
private final RabbitTemplate rabbitTemplate;
15+
private final ApplicationProperties properties;
16+
17+
OrderEventPublisher(RabbitTemplate rabbitTemplate, ApplicationProperties properties) {
18+
this.rabbitTemplate = rabbitTemplate;
19+
this.properties = properties;
20+
}
21+
22+
public void publish(OrderCreatedEvent event) {
23+
this.send(properties.newOrdersQueue(), event);
24+
}
25+
26+
private void send(String routingKey, Object payload) {
27+
rabbitTemplate.convertAndSend(properties.orderEventsExchange(), routingKey, payload);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.codebykavindu.bookstore.orders.domain;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
5+
/**
6+
* @author Kavindu Perera
7+
*/
8+
interface OrderEventRepository extends JpaRepository<OrderEventEntity, Long> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.codebykavindu.bookstore.orders.domain;
2+
3+
import com.codebykavindu.bookstore.orders.domain.models.OrderCreatedEvent;
4+
import com.codebykavindu.bookstore.orders.domain.models.OrderEventType;
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import java.util.List;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.data.domain.Sort;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
/**
15+
* @author Kavindu Perera
16+
*/
17+
@Service
18+
@Transactional
19+
public class OrderEventService {
20+
private static final Logger log = LoggerFactory.getLogger(OrderEventService.class);
21+
22+
private final OrderEventRepository orderEventRepository;
23+
private final OrderEventPublisher orderEventPublisher;
24+
private final ObjectMapper objectMapper;
25+
26+
OrderEventService(
27+
OrderEventRepository orderEventRepository,
28+
ObjectMapper objectMapper,
29+
OrderEventPublisher orderEventPublisher) {
30+
this.orderEventRepository = orderEventRepository;
31+
this.objectMapper = objectMapper;
32+
this.orderEventPublisher = orderEventPublisher;
33+
}
34+
35+
void save(OrderCreatedEvent event) {
36+
OrderEventEntity orderEvent = new OrderEventEntity();
37+
orderEvent.setEventId(event.eventId());
38+
orderEvent.setEventType(OrderEventType.ORDER_CREATED);
39+
orderEvent.setOrderNumber(event.orderNumber());
40+
orderEvent.setCreatedAt(event.createdAt());
41+
orderEvent.setPayload(toJsonPayload(event));
42+
this.orderEventRepository.save(orderEvent);
43+
}
44+
45+
public void publishOrderEvents() {
46+
Sort sort = Sort.by("createdAt").ascending();
47+
List<OrderEventEntity> events = orderEventRepository.findAll(sort);
48+
log.info("Found {} Order Events to publish", events.size());
49+
for (OrderEventEntity event : events) {
50+
this.publishEvent(event);
51+
orderEventRepository.delete(event);
52+
}
53+
}
54+
55+
private void publishEvent(OrderEventEntity event) {
56+
OrderEventType eventType = event.getEventType();
57+
switch (eventType) {
58+
case ORDER_CREATED:
59+
OrderCreatedEvent orderCreatedEvent = fromJsonPayload(event.getPayload(), OrderCreatedEvent.class);
60+
orderEventPublisher.publish(orderCreatedEvent);
61+
break;
62+
default:
63+
log.warn("Unknown Event Type: {}", eventType);
64+
}
65+
}
66+
67+
private String toJsonPayload(Object object) {
68+
try {
69+
return objectMapper.writeValueAsString(object);
70+
} catch (JsonProcessingException e) {
71+
throw new RuntimeException(e);
72+
}
73+
}
74+
75+
private <T> T fromJsonPayload(String json, Class<T> type) {
76+
try {
77+
return objectMapper.readValue(json, type);
78+
} catch (JsonProcessingException e) {
79+
throw new RuntimeException(e);
80+
}
81+
}
82+
}

order-service/src/main/java/com/codebykavindu/bookstore/orders/domain/OrderService.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.codebykavindu.bookstore.orders.domain.models.CreateOrderRequest;
44
import com.codebykavindu.bookstore.orders.domain.models.CreateOrderResponse;
5+
import com.codebykavindu.bookstore.orders.domain.models.OrderCreatedEvent;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78
import org.springframework.stereotype.Service;
@@ -17,10 +18,12 @@ public class OrderService {
1718

1819
private final OrderRepository orderRepository;
1920
private final OrderValidator orderValidator;
21+
private final OrderEventService orderEventService;
2022

21-
OrderService(OrderRepository orderRepository, OrderValidator orderValidator) {
23+
OrderService(OrderRepository orderRepository, OrderValidator orderValidator, OrderEventService orderEventService) {
2224
this.orderRepository = orderRepository;
2325
this.orderValidator = orderValidator;
26+
this.orderEventService = orderEventService;
2427
}
2528

2629
public CreateOrderResponse createOrder(String userName, CreateOrderRequest request) {
@@ -29,6 +32,8 @@ public CreateOrderResponse createOrder(String userName, CreateOrderRequest reque
2932
newOrder.setUserName(userName);
3033
OrderEntity savedOrder = this.orderRepository.save(newOrder);
3134
log.info("Created Order with orderNumber={}", savedOrder.getOrderNumber());
35+
OrderCreatedEvent orderCreatedEvent = OrderEventMapper.buildOrderCreatedEvent(savedOrder);
36+
orderEventService.save(orderCreatedEvent);
3237
return new CreateOrderResponse(savedOrder.getOrderNumber());
3338
}
3439
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.codebykavindu.bookstore.orders.domain.models;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.Set;
5+
6+
/**
7+
* @author Kavindu Perera
8+
*/
9+
public record OrderCreatedEvent(
10+
String eventId,
11+
String orderNumber,
12+
Set<OrderItem> items,
13+
Customer customer,
14+
Address deliveryAddress,
15+
LocalDateTime createdAt) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.codebykavindu.bookstore.orders.domain.models;
2+
3+
/**
4+
* @author Kavindu Perera
5+
*/
6+
public enum OrderEventType {
7+
ORDER_CREATED,
8+
ORDER_DELIVERED,
9+
ORDER_CANCELLED,
10+
ORDER_PROCESSING_FAILED
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.codebykavindu.bookstore.orders.jobs;
2+
3+
import com.codebykavindu.bookstore.orders.domain.OrderEventService;
4+
import java.time.Instant;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.scheduling.annotation.Scheduled;
8+
import org.springframework.stereotype.Component;
9+
10+
/**
11+
* @author Kavindu Perera
12+
*/
13+
@Component
14+
class OrderEventPublishingJob {
15+
private static final Logger log = LoggerFactory.getLogger(OrderEventPublishingJob.class);
16+
17+
private final OrderEventService orderEventService;
18+
19+
OrderEventPublishingJob(OrderEventService orderEventService) {
20+
this.orderEventService = orderEventService;
21+
}
22+
23+
@Scheduled(cron = "${orders.publish-order-events-job-cron}")
24+
public void publishOrderEvents() {
25+
log.info("Publishing Order Events at {}", Instant.now());
26+
orderEventService.publishOrderEvents();
27+
}
28+
}

order-service/src/main/java/com/codebykavindu/bookstore/orders/web/controllers/RabbitMQController.java

-30
This file was deleted.

0 commit comments

Comments
 (0)