Files
2025-11-29 18:28:30 +08:00

14 KiB

Spring Boot Event-Driven Patterns - Examples

Comprehensive examples demonstrating event-driven architecture from basic local events to advanced distributed messaging.

Example 1: Basic Domain Events

A simple product lifecycle with domain events.

// Domain event
public class ProductCreatedEvent extends DomainEvent {
    private final String productId;
    private final String name;
    private final BigDecimal price;

    public ProductCreatedEvent(String productId, String name, BigDecimal price) {
        super();
        this.productId = productId;
        this.name = name;
        this.price = price;
    }

    // Getters
}

// Aggregate publishing events
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
    private String id;
    private String name;
    private BigDecimal price;
    
    @Transient
    private List<DomainEvent> domainEvents = new ArrayList<>();

    public static Product create(String name, BigDecimal price) {
        Product product = new Product();
        product.id = UUID.randomUUID().toString();
        product.name = name;
        product.price = price;
        
        // Publish domain event
        product.publishEvent(new ProductCreatedEvent(product.id, name, price));
        
        return product;
    }

    protected void publishEvent(DomainEvent event) {
        domainEvents.add(event);
    }

    public List<DomainEvent> getDomainEvents() {
        return new ArrayList<>(domainEvents);
    }

    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

Example 2: Local Event Publishing

Using ApplicationEventPublisher for in-process events.

// Application service
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class ProductApplicationService {
    private final ProductRepository productRepository;
    private final ApplicationEventPublisher eventPublisher;

    public ProductResponse createProduct(CreateProductRequest request) {
        Product product = Product.create(request.getName(), request.getPrice());
        Product saved = productRepository.save(product);
        
        // Publish domain events
        saved.getDomainEvents().forEach(event -> {
            log.debug("Publishing event: {}", event.getClass().getSimpleName());
            eventPublisher.publishEvent(event);
        });
        saved.clearDomainEvents();
        
        return mapper.toResponse(saved);
    }
}

// Event listener
@Component
@Slf4j
@RequiredArgsConstructor
public class ProductEventHandler {
    private final NotificationService notificationService;
    private final InventoryService inventoryService;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onProductCreated(ProductCreatedEvent event) {
        log.info("Handling ProductCreatedEvent");
        
        // Send notification
        notificationService.sendProductCreatedNotification(
            event.getName(), event.getPrice()
        );
        
        // Update inventory
        inventoryService.registerProduct(event.getProductId());
    }
}

// Test
@SpringBootTest
class ProductEventTest {
    @Autowired
    private ProductApplicationService productService;
    
    @MockBean
    private NotificationService notificationService;
    
    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldPublishProductCreatedEvent() {
        // Act
        productService.createProduct(
            new CreateProductRequest("Laptop", BigDecimal.valueOf(999.99))
        );

        // Assert - Event was handled
        verify(notificationService).sendProductCreatedNotification(
            "Laptop", BigDecimal.valueOf(999.99)
        );
    }
}

Example 3: Transactional Outbox Pattern

Ensures reliable event publishing even on failures.

// Outbox entity
@Entity
@Table(name = "outbox_events")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OutboxEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    private String aggregateId;
    private String eventType;
    
    @Column(columnDefinition = "TEXT")
    private String payload;
    
    private LocalDateTime createdAt;
    private LocalDateTime publishedAt;
    private Integer retryCount;
}

// Application service using outbox
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class ProductApplicationService {
    private final ProductRepository productRepository;
    private final OutboxEventRepository outboxRepository;
    private final ObjectMapper objectMapper;

    public ProductResponse createProduct(CreateProductRequest request) {
        Product product = Product.create(request.getName(), request.getPrice());
        Product saved = productRepository.save(product);
        
        // Store event in outbox (same transaction)
        saved.getDomainEvents().forEach(event -> {
            try {
                String payload = objectMapper.writeValueAsString(event);
                OutboxEvent outboxEvent = OutboxEvent.builder()
                    .aggregateId(saved.getId())
                    .eventType(event.getClass().getSimpleName())
                    .payload(payload)
                    .createdAt(LocalDateTime.now())
                    .retryCount(0)
                    .build();
                
                outboxRepository.save(outboxEvent);
                log.debug("Outbox event created: {}", event.getClass().getSimpleName());
            } catch (Exception e) {
                log.error("Failed to create outbox event", e);
                throw new RuntimeException(e);
            }
        });
        
        return mapper.toResponse(saved);
    }
}

// Scheduled publisher
@Component
@Slf4j
@RequiredArgsConstructor
public class OutboxEventPublisher {
    private final OutboxEventRepository outboxRepository;
    private final KafkaTemplate<String, String> kafkaTemplate;
    private final ObjectMapper objectMapper;

    @Scheduled(fixedDelay = 5000)
    @Transactional
    public void publishPendingEvents() {
        List<OutboxEvent> pending = outboxRepository.findByPublishedAtIsNull();
        
        for (OutboxEvent event : pending) {
            try {
                kafkaTemplate.send("product-events", 
                    event.getAggregateId(), event.getPayload());
                
                event.setPublishedAt(LocalDateTime.now());
                outboxRepository.save(event);
                
                log.info("Published outbox event: {}", event.getId());
            } catch (Exception e) {
                log.error("Failed to publish event: {}", event.getId(), e);
                event.setRetryCount(event.getRetryCount() + 1);
                outboxRepository.save(event);
            }
        }
    }
}

Example 4: Kafka Event Publishing

Distributed event publishing with Spring Cloud Stream.

// Application configuration
@Configuration
public class KafkaConfig {
    
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

// Event publisher
@Component
@Slf4j
@RequiredArgsConstructor
public class KafkaProductEventPublisher {
    private final KafkaTemplate<String, Object> kafkaTemplate;

    public void publishProductCreatedEvent(ProductCreatedEvent event) {
        log.info("Publishing ProductCreatedEvent to Kafka: {}", event.getProductId());
        
        kafkaTemplate.send("product-events", 
            event.getProductId(),
            event);
    }
}

// Event consumer
@Component
@Slf4j
@RequiredArgsConstructor
public class ProductEventStreamConsumer {
    private final InventoryService inventoryService;

    @Bean
    public java.util.function.Consumer<ProductCreatedEvent> productCreatedConsumer() {
        return event -> {
            log.info("Consumed ProductCreatedEvent: {}", event.getProductId());
            inventoryService.registerProduct(event.getProductId(), event.getName());
        };
    }

    @Bean
    public java.util.function.Consumer<ProductUpdatedEvent> productUpdatedConsumer() {
        return event -> {
            log.info("Consumed ProductUpdatedEvent: {}", event.getProductId());
            inventoryService.updateProduct(event.getProductId(), event.getPrice());
        };
    }
}

// Application properties

application.yml:

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      group-id: product-service
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "*"

  cloud:
    stream:
      bindings:
        productCreatedConsumer-in-0:
          destination: product-events
          group: product-inventory-service
        productUpdatedConsumer-in-0:
          destination: product-events
          group: product-inventory-service

Example 5: Event Saga Pattern

Coordinating multiple services with events.

// Events
public class OrderPlacedEvent extends DomainEvent {
    private final String orderId;
    private final String productId;
    private final Integer quantity;
    // ...
}

public class OrderPaymentConfirmedEvent extends DomainEvent {
    private final String orderId;
    // ...
}

// Saga orchestrator
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderFulfillmentSaga {
    private final OrderService orderService;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        log.info("Starting order fulfillment saga for order: {}", event.getOrderId());
        
        try {
            // Step 1: Reserve inventory
            inventoryService.reserveStock(event.getProductId(), event.getQuantity());
            log.info("Inventory reserved for order: {}", event.getOrderId());
            
            // Step 2: Process payment
            paymentService.processPayment(event.getOrderId());
            log.info("Payment processed for order: {}", event.getOrderId());
            
            // Step 3: Publish confirmation
            eventPublisher.publishEvent(new OrderPaymentConfirmedEvent(event.getOrderId()));
            
            // Step 4: Update order status
            orderService.markAsConfirmed(event.getOrderId());
            log.info("Order confirmed: {}", event.getOrderId());
            
        } catch (PaymentFailedException e) {
            log.warn("Payment failed, releasing inventory");
            inventoryService.releaseStock(event.getProductId(), event.getQuantity());
            orderService.markAsFailed(event.getOrderId(), e.getMessage());
        }
    }
}

// Test
@SpringBootTest
class OrderFulfillmentSagaTest {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    @MockBean
    private InventoryService inventoryService;
    
    @MockBean
    private PaymentService paymentService;
    
    @MockBean
    private OrderService orderService;

    @Test
    void shouldCompleteOrderFulfillmentSaga() {
        // Arrange
        OrderPlacedEvent event = new OrderPlacedEvent("order-123", "product-456", 2);

        // Act
        eventPublisher.publishEvent(event);

        // Assert
        verify(inventoryService).reserveStock("product-456", 2);
        verify(paymentService).processPayment("order-123");
        verify(orderService).markAsConfirmed("order-123");
    }
}

Example 6: Event Sourcing Foundation

Storing state changes as events.

// Event store
@Repository
public interface EventStoreRepository extends JpaRepository<StoredEvent, UUID> {
    List<StoredEvent> findByAggregateIdOrderBySequenceAsc(String aggregateId);
}

// Stored event
@Entity
@Table(name = "event_store")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StoredEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    private String aggregateId;
    private String eventType;
    private Integer sequence;
    
    @Column(columnDefinition = "TEXT")
    private String payload;
    
    private LocalDateTime occurredAt;
}

// Event sourcing service
@Service
@Slf4j
@RequiredArgsConstructor
public class EventSourcingService {
    private final EventStoreRepository eventStoreRepository;
    private final ObjectMapper objectMapper;

    @Transactional
    public void storeEvent(String aggregateId, DomainEvent event) {
        try {
            List<StoredEvent> existing = eventStoreRepository
                .findByAggregateIdOrderBySequenceAsc(aggregateId);
            
            Integer nextSequence = existing.isEmpty() ? 1 : 
                existing.get(existing.size() - 1).getSequence() + 1;
            
            StoredEvent storedEvent = StoredEvent.builder()
                .aggregateId(aggregateId)
                .eventType(event.getClass().getSimpleName())
                .sequence(nextSequence)
                .payload(objectMapper.writeValueAsString(event))
                .occurredAt(LocalDateTime.now())
                .build();
            
            eventStoreRepository.save(storedEvent);
            log.info("Event stored: {} for aggregate: {}", 
                event.getClass().getSimpleName(), aggregateId);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to store event", e);
        }
    }

    public List<DomainEvent> getEventHistory(String aggregateId) {
        return eventStoreRepository
            .findByAggregateIdOrderBySequenceAsc(aggregateId)
            .stream()
            .map(this::deserializeEvent)
            .collect(Collectors.toList());
    }

    private DomainEvent deserializeEvent(StoredEvent stored) {
        try {
            Class<?> eventClass = Class.forName(
                "com.example.product.domain.event." + stored.getEventType());
            return (DomainEvent) objectMapper.readValue(stored.getPayload(), eventClass);
        } catch (Exception e) {
            throw new RuntimeException("Failed to deserialize event", e);
        }
    }
}

These examples cover local events, transactional outbox pattern, Kafka publishing, saga coordination, and event sourcing foundations for comprehensive event-driven architecture.