Appearance
Events API
This document provides a detailed technical overview of the event publishing mechanism within the ticketing system. It is intended for developers who need to understand, maintain, or consume the events generated by this service.
The system uses an event-driven approach to communicate state changes for ticket entities. These events are published to a message broker, allowing other services to react to ticket creations and updates in a decoupled manner. For a broader overview of this pattern, refer to the Event-Driven Architecture concepts.
Event Structure
All events related to tickets follow a standardized structure defined by the TicketEvent class. This ensures that consumers can reliably parse and handle incoming messages. The event object is a plain Java object (POJO) that is serialized to JSON before being published.
The structure is defined in src/main/java/com/slalom/demo/ticketing/event/TicketEvent.java:
java
package com.slalom.demo.ticketing.event;
import com.slalom.demo.ticketing.model.TicketPriority;
import com.slalom.demo.ticketing.model.TicketStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TicketEvent {
private String eventType; // CREATED, UPDATED
private Long ticketId;
private String title;
private TicketStatus status;
private TicketPriority priority;
private String requesterEmail;
private String assignedTo;
private LocalDateTime timestamp;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Payload Fields
The JSON payload contains a snapshot of the ticket's state at the time the event was generated.
| Field | Type | Description |
|---|---|---|
eventType | String | The type of event that occurred. See Event Types below. |
ticketId | Long | The unique identifier of the ticket associated with the event. |
title | String | The title of the ticket. |
status | String | The status of the ticket at the time of the event (e.g., OPEN, IN_PROGRESS, RESOLVED). |
priority | String | The priority of the ticket at the time of the event (e.g., LOW, MEDIUM, HIGH). |
requesterEmail | String | The email address of the user who created the ticket. |
assignedTo | String | The identifier of the user or group the ticket is assigned to. Can be null. |
timestamp | ISO 8601 String | The timestamp (UTC) indicating when the event was generated by the service. |
Sample JSON Payload
json
{
"eventType": "CREATED",
"ticketId": 101,
"title": "Fix login button alignment",
"status": "OPEN",
"priority": "MEDIUM",
"requesterEmail": "user@example.com",
"assignedTo": "dev-team@example.com",
"timestamp": "2023-10-27T10:00:00.123456"
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Event Types
The eventType field distinguishes between different lifecycle events of a ticket.
CREATED: Published when a new ticket is successfully created and persisted in the database. This event is triggered from within theTicketService.createTicketmethod.UPDATED: Published when an existing ticket's attributes are modified and the changes are persisted. This event is triggered from within theTicketService.updateTicketmethod.
Note: No event is published when a ticket is deleted via the
TicketService.deleteTicketmethod. Consumers that maintain a local cache or replica of ticket data must implement a different strategy to handle deletions, such as periodically checking for the existence of tickets or using a soft-delete pattern if one were to be implemented.
Event Publishing
Event publishing is tightly integrated with the core business logic in the TicketService to ensure data consistency.
Publishing Flow
- A client calls an endpoint that triggers either
TicketService.createTicket()orTicketService.updateTicket(). - The
TicketServiceperforms the database operation (INSERT or UPDATE) on theTicketentity. - After the database operation is successful, the service constructs a
TicketEventobject. - The
TicketServicecalls theMessagePublisher.publishTicketEvent()method, passing the newly created event object. - The
MessagePublisheruses Spring'sRabbitTemplateto serialize theTicketEventto JSON and send it to the configured RabbitMQ exchange.
The TicketService contains a private helper method to build and publish the event:
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
private void publishTicketEvent(Ticket ticket, String eventType) {
// Build the event object from the persisted ticket entity
TicketEvent event = TicketEvent.builder()
.eventType(eventType)
.ticketId(ticket.getId())
.title(ticket.getTitle())
.status(ticket.getStatus())
.priority(ticket.getPriority())
.requesterEmail(ticket.getRequesterEmail())
.assignedTo(ticket.getAssignedTo())
.timestamp(LocalDateTime.now())
.build();
// Delegate publishing to the dedicated MessagePublisher service
messagePublisher.publishTicketEvent(event);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Transactional Integrity
Both the createTicket and updateTicket methods are annotated with @Transactional. This ensures that the database operations are atomic and will be rolled back if an exception occurs.
Important: The
@Transactionalannotation applies to the database operations only. By default, RabbitMQ message publishing viaRabbitTemplateis not automatically included in the database transaction. This means:
- If the database save succeeds but the message publish fails, the database changes will still be committed.
- Conversely, if the message is published but the database operation fails, the transaction will roll back, but the message may already be in the broker.
- This can result in inconsistent states where a ticket is created/updated in the database but no event is published, or vice versa.
Mitigation: To achieve true transactional integrity between database and messaging operations, additional patterns such as the transactional outbox pattern or Spring's transaction synchronization mechanisms would need to be implemented. Consumers should be designed to handle potential inconsistencies gracefully.
Message Publisher Service
The responsibility of communicating with the message broker is encapsulated in the MessagePublisher service. This separation of concerns keeps the TicketService focused on business logic.
java
// src/main/java/com/slalom/demo/ticketing/service/MessagePublisher.java
@Service
@RequiredArgsConstructor
@Slf4j
public class MessagePublisher {
private final RabbitTemplate rabbitTemplate;
// Exchange and routing key are configured externally
@Value("${rabbitmq.exchange.name}")
private String exchangeName;
@Value("${rabbitmq.routing.key}")
private String routingKey;
public void publishTicketEvent(TicketEvent event) {
log.info("Publishing ticket event: {} for ticket ID: {}", event.getEventType(), event.getTicketId());
// convertAndSend handles object-to-JSON serialization and sends the message
rabbitTemplate.convertAndSend(exchangeName, routingKey, event);
log.info("Successfully published event to exchange: {}", exchangeName);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
The exchange name and routing key are externalized into application properties, allowing for flexible configuration per environment. For details on configuring these values, see the RabbitMQ Configuration documentation.
Consuming Events
Services that wish to consume ticket events must configure their AMQP listeners to subscribe to the appropriate exchange.
RabbitMQ Queue Configuration
Consumers are responsible for declaring their own queue and binding it to the exchange defined by the publisher's ${rabbitmq.exchange.name} property. This is typically a topic exchange to allow for flexible routing, although the current implementation uses a single routing key.
Routing Key Patterns
The publisher currently sends all events (CREATED and UPDATED) with the same static routing key, defined by ${rabbitmq.routing.key}. Consumers should bind their queue to the exchange using this exact routing key.
Future Enhancement: A potential improvement would be to use more descriptive routing keys, such as
ticket.createdorticket.updated. This would allow consumers to subscribe only to the event types they are interested in, reducing unnecessary message processing.
Message Deserialization
The message body will be a JSON string. Consumers should be configured to deserialize this JSON into a TicketEvent DTO or a compatible class. Spring AMQP can handle this automatically if the consumer is also a Spring application with the TicketEvent class on its classpath and a properly configured MessageConverter.
Idempotency and Error Handling
Consumers must be designed to be idempotent, as message brokers can, under certain failure scenarios, deliver a message more than once.
- Idempotency: A common strategy is to track the
ticketIdandtimestampof processed events. Before processing a new event, the consumer should check if an event with the sameticketIdand a more recent or equaltimestamphas already been handled. - Error Handling: Implement robust error handling and dead-letter queue (DLQ) configurations to handle messages that cannot be processed successfully.
For more detailed guidance on building robust message consumers, refer to the Messaging Development Guide.