Declarative Event Publishing¶
The @PublishEvent annotation is the core feature of Curve, enabling declarative event publishing with minimal code.
Basic Usage¶
import io.github.closeup1202.curve.spring.audit.annotation.PublishEvent;
@Service
public class OrderService {
@PublishEvent(eventType = "ORDER_CREATED")
public Order createOrder(OrderRequest request) {
return orderRepository.save(new Order(request));
}
}
When createOrder() is called, Curve automatically:
- Captures the method return value (
Order) - Extracts metadata (trace ID, user, etc.)
- Wraps it in
EventEnvelope - Publishes to Kafka
Annotation Parameters¶
Required Parameters¶
eventType (String)¶
Unique identifier for this event type.
Naming conventions:
- Use SCREAMING_SNAKE_CASE
- Be specific:
ORDER_CREATEDnot justCREATED - Include entity name:
USER_DELETED,PAYMENT_COMPLETED
Optional Parameters¶
severity (EventSeverity)¶
Event severity level for filtering and alerting.
Available values:
INFO- Normal operations (default)WARN- WarningsERROR- Errors requiring attentionCRITICAL- Critical failures requiring immediate action
payload (SpEL Expression)¶
Extract specific data for the event payload using Spring Expression Language.
@PublishEvent(
eventType = "USER_UPDATED",
payload = "#args[0].toEventDto()" // Transform request
)
public User updateUser(UserUpdateRequest request) {
return userRepository.save(request.toEntity());
}
SpEL Variables:
| Variable | Description | Example |
|---|---|---|
#result | Method return value | #result |
#args[n] | Method arguments | #args[0], #args[1] |
#root | Root evaluation context | #root.methodName |
Examples:
// Use entire return value (default)
@PublishEvent(eventType = "ORDER_CREATED")
public Order createOrder(OrderRequest req) { ... }
// Use specific field
@PublishEvent(
eventType = "ORDER_CREATED",
payload = "#result.id"
)
public Order createOrder(OrderRequest req) { ... }
// Transform with custom method
@PublishEvent(
eventType = "USER_CREATED",
payload = "#result.toPublicDto()"
)
public User createUser(UserRequest req) { ... }
// Use method argument
@PublishEvent(
eventType = "ORDER_SUBMITTED",
payload = "#args[0]"
)
public Order submitOrder(OrderSubmission submission) { ... }
payloadIndex (int)¶
Specify which method parameter to use as the event payload.
@PublishEvent(
eventType = "ORDER_SUBMITTED",
payloadIndex = 0 // Use first parameter (0-indexed)
)
public Order submitOrder(OrderSubmission submission) {
// submission will be used as payload
return orderRepository.save(submission.toOrder());
}
Values: - -1 (default): Use method return value as payload - 0 or greater: Use the parameter at this index as payload
Note: The payload SpEL expression overrides this setting if specified.
phase (PublishEvent.Phase)¶
Control when the event is published relative to method execution.
@PublishEvent(
eventType = "VALIDATION_PERFORMED",
phase = PublishEvent.Phase.BEFORE // Publish before method runs
)
public void validateOrder(Order order) {
// Event published first, then validation runs
validator.validate(order);
}
Available phases:
| Phase | When Published | Use Case |
|---|---|---|
BEFORE | Before method execution | Pre-validation events, audit trails |
AFTER_RETURNING | After successful return (default) | Success events, state changes |
AFTER | After method execution (even on exception) | Audit trails regardless of outcome |
Example scenarios:
// Success-only events
@PublishEvent(
eventType = "ORDER_CREATED",
phase = PublishEvent.Phase.AFTER_RETURNING
)
public Order createOrder(OrderRequest req) { ... }
// Always publish, even on failure
@PublishEvent(
eventType = "ORDER_CREATION_ATTEMPTED",
phase = PublishEvent.Phase.AFTER
)
public Order createOrder(OrderRequest req) { ... }
failOnError (boolean)¶
Control whether event publishing failures should fail the business logic.
@PublishEvent(
eventType = "CRITICAL_OPERATION",
failOnError = true // Throw exception if event publishing fails
)
public void performCriticalOperation() {
// If event publishing fails, this method will throw exception
// and rollback any transaction
}
Values: - false (default): Log error but continue business logic - true: Throw exception and fail the method if event publishing fails
Recommendation: Use false (default) for most cases to prevent event publishing issues from breaking business logic. Only use true for critical audit requirements where event loss is unacceptable.
Transactional Outbox Parameters¶
For guaranteed delivery with transactional outbox pattern:
@Transactional
@PublishEvent(
eventType = "ORDER_CREATED",
outbox = true, // Enable outbox
aggregateType = "Order", // Entity type
aggregateId = "#result.id" // Entity ID
)
public Order createOrder(OrderRequest request) {
return orderRepository.save(new Order(request));
}
Parameters:
| Parameter | Type | Description |
|---|---|---|
outbox | boolean | Enable transactional outbox |
aggregateType | String | Entity type name |
aggregateId | SpEL | Entity unique identifier |
Advanced Examples¶
1. Multi-Parameter Method¶
@PublishEvent(
eventType = "ORDER_SHIPPED",
payload = "#result.toShipmentPayload()"
)
public Shipment shipOrder(Long orderId, Address address) {
// ...
return shipment;
}
2. Conditional Publishing¶
Use Spring's conditional annotations:
@ConditionalOnProperty(name = "features.audit", havingValue = "true")
@PublishEvent(eventType = "ADMIN_ACTION")
public void performAdminAction(AdminRequest request) {
// ...
}
3. Method-Level Configuration¶
Override global settings per method:
@Service
public class CriticalService {
// High-priority event with custom severity and error handling
@PublishEvent(
eventType = "FRAUD_DETECTED",
severity = EventSeverity.CRITICAL,
failOnError = true // Fail method if event cannot be published
)
public FraudAlert detectFraud(Transaction tx) {
// Critical audit event - must be published
return fraudDetectionService.analyze(tx);
}
}
4. Async Method Publishing¶
Works with @Async methods:
@Async
@PublishEvent(eventType = "REPORT_GENERATED")
public CompletableFuture<Report> generateReport(ReportRequest req) {
Report report = reportGenerator.generate(req);
return CompletableFuture.completedFuture(report);
}
MDC Context Propagation
Curve automatically propagates MDC context (trace ID, etc.) to async threads.
5. Multi-Topic Publishing¶
Route different event types to different Kafka topics based on domain context:
@Service
public class ECommerceService {
// Route cart events to cart topic
@PublishEvent(
eventType = "CART_ITEM_ADDED",
topic = "cart.events",
payload = "#result.toCartEventDto()"
)
public CartItem addToCart(CartRequest request) {
return cartRepository.save(new CartItem(request));
}
// Route inventory events to stock topic
@PublishEvent(
eventType = "STOCK_DECREASED",
topic = "stock.events",
payload = "#result"
)
public Stock decreaseInventory(StockDecreaseRequest request) {
return inventoryService.decreaseStock(request);
}
// Uses default topic (curve.kafka.topic) when topic not specified
@PublishEvent(eventType = "ORDER_CREATED")
public Order createOrder(OrderRequest request) {
return orderRepository.save(new Order(request));
}
}
Topic Resolution:
- If
topicattribute is set → publish to specified topic - If
topicis empty or not specified → usecurve.kafka.topic(default topic from configuration)
Benefits:
- Domain Isolation: Keep cart, inventory, and order events in separate streams
- Scalability: Different topics can have different partition counts for throughput optimization
- Consumer Flexibility: Different consumer groups can subscribe to specific topics
- Backward Compatibility: Existing code without
topicattribute continues to use the default topic
Best Practices¶
DO¶
- Use descriptive event types:
USER_REGISTERED,ORDER_COMPLETED - Apply on service layer methods (not controllers or repositories)
- Keep payload minimal - only essential data
- Use
@PiiFieldfor sensitive data - Set appropriate severity levels
DON'T¶
- Publish high-volume events in sync mode (use async)
- Include entire entities as payload (extract DTOs)
- Publish from controllers (breaks separation of concerns)
- Use generic event types like
CREATEDorUPDATED
Troubleshooting¶
Events Not Publishing¶
Events not appearing in Kafka
Check:
curve.enabled=truein application.yml- Method is called through Spring proxy (not
this.method()) - No exceptions thrown before method completes
- Kafka connection is healthy
Debug:
Payload Extraction Fails¶
SpEL evaluation error
Common issues:
- Typo in SpEL expression
- Accessing null fields
- Wrong argument index
Solution:
What's Next?¶
-
PII Protection
Automatically protect sensitive data
-
Transactional Outbox
Guarantee exactly-once delivery
-
API Reference
Complete annotation reference