Appearance
Service Layer
This document provides a detailed technical overview of the service layer for the ticketing module. The service layer is responsible for encapsulating the core business logic, coordinating data persistence, and orchestrating interactions with other application components like the messaging system. The primary component in this layer is the TicketService.
Business Logic
The TicketService class acts as the central hub for all business operations related to tickets. It is a Spring @Service that orchestrates data access, validation, and event publishing.
TicketService Implementation
The service is designed with dependency injection in mind, using Lombok's @RequiredArgsConstructor to inject its dependencies: TicketRepository for data access and MessagePublisher for asynchronous eventing.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
@Service
@RequiredArgsConstructor // Injects dependencies via the constructor
@Slf4j
public class TicketService {
private final TicketRepository ticketRepository;
private final MessagePublisher messagePublisher;
// ... method implementations
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
CRUD Operation Handling
The service provides standard Create, Read, Update, and Delete (CRUD) methods. All public methods that modify data are annotated with @Transactional to ensure atomicity. Read-only operations are optimized with @Transactional(readOnly = true).
- Create:
createTicket(TicketRequest)creates a newTicketentity from a request DTO, persists it, and publishes aCREATEDevent. - Read:
getTicket(id),getAllTickets(),getTicketsByStatus(status), andgetTicketsByRequester(email)provide various ways to query for tickets. These methods fetch data from the repository and map it toTicketResponseDTOs. - Update:
updateTicket(id, TicketRequest)fetches an existing ticket, applies the updates from the request DTO, persists the changes, and publishes anUPDATEDevent. - Delete:
deleteTicket(id)removes a ticket from the database. It first checks for the ticket's existence to provide a clear error message if it's not found. Note that unlike create and update operations, delete does not publish an event to the messaging system.
Resolved Timestamp Logic
A key piece of business logic is handled within the updateTicket method. When a ticket's status is changed to RESOLVED or CLOSED, the system must record the time of resolution.
The implementation ensures that the resolvedAt timestamp is set only once. This prevents the timestamp from being overwritten if the ticket is modified again after being resolved.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
@Transactional
public TicketResponse updateTicket(Long id, TicketRequest request) {
// ... fetch ticket logic ...
// ... update other fields ...
ticket.setStatus(request.getStatus());
// Set resolved timestamp if status changed to RESOLVED or CLOSED
// and if the timestamp has not been set previously.
if ((request.getStatus() == TicketStatus.RESOLVED || request.getStatus() == TicketStatus.CLOSED)
&& ticket.getResolvedAt() == null) {
ticket.setResolvedAt(LocalDateTime.now());
}
Ticket updatedTicket = ticketRepository.save(ticket);
// ... publish event and return response ...
return toResponse(updatedTicket);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DTO Mapping
The service layer uses the Data Transfer Object (DTO) pattern to decouple the internal domain model (Ticket) from the external API contract (TicketRequest, TicketResponse). This provides a layer of security and flexibility.
Request to Entity Conversion
When creating or updating a ticket, the incoming TicketRequest DTO is manually mapped to a Ticket entity. This is done using the builder pattern provided by Lombok, which offers clear and readable object construction.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
// Example from createTicket method
Ticket ticket = Ticket.builder()
.title(request.getTitle())
.description(request.getDescription())
.status(request.getStatus())
.priority(request.getPriority())
.requesterEmail(request.getRequesterEmail())
.assignedTo(request.getAssignedTo())
.build();
Ticket savedTicket = ticketRepository.save(ticket);1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Entity to Response Conversion
Conversely, when returning data to the client, Ticket entities are converted to TicketResponse DTOs. A private helper method, toResponse(Ticket), centralizes this mapping logic, ensuring consistency across all methods that return ticket data.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
private TicketResponse toResponse(Ticket ticket) {
return TicketResponse.builder()
.id(ticket.getId())
.title(ticket.getTitle())
.description(ticket.getDescription())
.status(ticket.getStatus())
.priority(ticket.getPriority())
.requesterEmail(ticket.getRequesterEmail())
.assignedTo(ticket.getAssignedTo())
.createdAt(ticket.getCreatedAt()) // Mapped from entity's audited field
.updatedAt(ticket.getUpdatedAt()) // Mapped from entity's audited field
.resolvedAt(ticket.getResolvedAt())
.build();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The Ticket entity's createdAt and updatedAt fields, likely managed by Spring Data JPA's auditing capabilities, are directly mapped to the response. For more details on the data layer, see the Data Access documentation.
Event Publishing
The service layer integrates with a messaging system to publish events for significant state changes, such as ticket creation and updates. This enables other microservices or system components to react to ticket activity in a decoupled, asynchronous manner.
Integration with MessagePublisher
The TicketService relies on a MessagePublisher interface, which is injected as a dependency. This interface abstracts the details of the messaging implementation. Based on the project's technical stack, this publisher sends messages to a RabbitMQ exchange.
For more information on the messaging implementation, refer to the Messaging documentation.
Event Creation and Publishing
A private helper method, publishTicketEvent, is responsible for creating a TicketEvent object and passing it to the MessagePublisher. This method standardizes the event creation process.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
private void publishTicketEvent(Ticket ticket, String eventType) {
// 1. Build the event payload 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();
// 2. Delegate publishing to the MessagePublisher component
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
Transaction Boundaries for Events
A critical design choice is placing the publishTicketEvent call within the same transaction as the database save operation.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
@Transactional
public TicketResponse createTicket(TicketRequest request) {
// ... create and save ticket ...
Ticket savedTicket = ticketRepository.save(ticket);
// Publish event within the same transaction
publishTicketEvent(savedTicket, "CREATED");
return toResponse(savedTicket);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Implications:
- Success Scenario: The database commit and the message publication are treated as a single atomic unit.
- Failure Scenario: If
messagePublisher.publishTicketEvent(event)throws an exception, the@Transactionalannotation ensures that the entire transaction is rolled back. This means theticketRepository.save(ticket)operation will be undone, preventing a "ghost" ticket in the database without a corresponding creation event. This pattern guarantees consistency between the application's database state and the events stream.
Error Handling
The service employs several strategies for handling errors and invalid data.
RuntimeException for Not Found
For operations on a specific ticket (e.g., getTicket, updateTicket), the service first attempts to fetch the entity by its ID. If the ticket does not exist, ticketRepository.findById(id) returns an empty Optional. The .orElseThrow() method is used to immediately fail the operation by throwing a RuntimeException.
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
Ticket ticket = ticketRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Ticket not found with id: " + id));1
2
3
4
2
3
4
Note: While functional, this approach could be enhanced. A best practice would be to define a custom, more specific exception (e.g., TicketNotFoundException) and have a global @ControllerAdvice handle it to return a proper 404 Not Found HTTP status. For details on the API contract, see the API Reference.
Validation Before Persistence
Input validation is handled declaratively on the TicketRequest DTO using Jakarta Bean Validation annotations.
java
// src/main/java/com/slalom/demo/ticketing/dto/TicketRequest.java
public class TicketRequest {
@NotBlank(message = "Title is required")
private String title;
@NotBlank(message = "Description is required")
private String description;
@NotNull(message = "Status is required")
private TicketStatus status;
@NotNull(message = "Priority is required")
private TicketPriority priority;
@NotBlank(message = "Requester email is required")
@Email(message = "Invalid email format")
private String requesterEmail;
// ... other fields
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
These validation rules are typically enforced by the web layer (Controller) before the request ever reaches the TicketService. This "fail-fast" approach prevents invalid data from entering the business logic layer.
Transaction Rollback Scenarios
The use of @Transactional on public service methods provides a robust safety net. Any uncaught RuntimeException thrown from within a transactional method will trigger a transaction rollback. This includes:
- A
RuntimeExceptionthrown when a ticket is not found during an update. - Exceptions thrown by the JPA provider (e.g., database constraint violations).
- Exceptions thrown during event publishing, as detailed in the section above.
This ensures that the application's state remains consistent in the face of unexpected errors.