Appearance
Components
This document provides a detailed technical breakdown of the primary software components within the ticketing application. It is intended for developers responsible for maintaining and extending the system. For a higher-level view of how these components interact, please refer to the Architecture Overview.
Controllers
The controller layer is the entry point for all incoming HTTP requests. It is responsible for request routing, input validation, and delegating business logic execution to the service layer.
TicketController
The TicketController (com.slalom.demo.ticketing.controller.TicketController) manages all RESTful operations related to tickets. It is annotated with @RestController, which combines @Controller and @ResponseBody, ensuring that return values from its methods are automatically serialized into JSON and written to the HTTP response body.
Key Responsibilities:
- Endpoint Mapping: Maps HTTP methods and URI patterns to specific handler methods using annotations like
@GetMapping,@PostMapping,@PutMapping, and@DeleteMapping. The base path for all ticket-related endpoints is/api/tickets. - Request Handling: Deserializes incoming JSON request bodies into
TicketRequestDTOs using@RequestBody. It also extracts data from the URL path (@PathVariable) and query string (@RequestParam). - Input Validation: Enforces validation rules defined on the
TicketRequestDTO by using the@Validannotation. If validation fails, Spring Boot automatically triggers aMethodArgumentNotValidException, which results in a400 Bad Requestresponse. - Response Management: Wraps responses in
ResponseEntityobjects to provide full control over the HTTP status code and headers. For example, successful creation returns201 Created, while successful deletion returns204 No Content. - Delegation: The controller's primary function is to act as a thin layer that delegates all business logic to the
TicketService. It does not contain any business logic itself.
Code Snippet: Update and Filtering Endpoints
java
// src/main/java/com/slalom/demo/ticketing/controller/TicketController.java
@RestController
@RequestMapping("/api/tickets")
@RequiredArgsConstructor
@Slf4j
public class TicketController {
private final TicketService ticketService;
// Example of a GET endpoint with optional filtering parameters
@GetMapping
public ResponseEntity<List<TicketResponse>> getAllTickets(
@RequestParam(required = false) TicketStatus status,
@RequestParam(required = false) String requesterEmail) {
log.info("REST: Fetching tickets with filters - status: {}, requesterEmail: {}", status, requesterEmail);
List<TicketResponse> tickets;
if (status != null) {
tickets = ticketService.getTicketsByStatus(status);
} else if (requesterEmail != null) {
tickets = ticketService.getTicketsByRequester(requesterEmail);
} else {
tickets = ticketService.getAllTickets();
}
return ResponseEntity.ok(tickets);
}
// Example of a PUT endpoint with path variable and validated request body
@PutMapping("/{id}")
public ResponseEntity<TicketResponse> updateTicket(
@PathVariable Long id,
@Valid @RequestBody TicketRequest request) {
log.info("REST: Updating ticket with ID: {}", id);
TicketResponse response = ticketService.updateTicket(id, request);
return ResponseEntity.ok(response);
}
// ... other endpoints
}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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
For a complete list of all available endpoints, their request/response formats, and example usage, please see the Ticket API Reference.
Services
The service layer contains the core business logic of the application. It orchestrates interactions between the data access layer (repositories) and other parts of the system, such as message brokers.
TicketService
The TicketService (com.slalom.demo.ticketing.service.TicketService) implements all business operations for managing tickets. It is annotated with @Service, marking it as a candidate for Spring's component scanning.
Key Responsibilities & Patterns:
- Transactional Logic: All public methods are annotated with
@Transactional. This ensures that database operations within a single method call are executed within a single transaction.- Write operations (e.g.,
createTicket,updateTicket) use the default read-write transaction. - Read operations (e.g.,
getTicket,getAllTickets) are optimized with@Transactional(readOnly = true), which provides performance benefits at the JPA and database level.
- Write operations (e.g.,
- Business Rule Enforcement: The service layer is where business rules are implemented. A key example is the logic to set the
resolvedAttimestamp when a ticket's status is changed toRESOLVEDorCLOSED. - Entity-DTO Mapping: It is responsible for converting incoming
TicketRequestDTOs intoTicketdomain entities for persistence, and mappingTicketentities toTicketResponseDTOs before returning them to the controller. This is handled by the privatetoResponsemethod and builder patterns. - Event Publishing: After a ticket is created or updated, the
TicketServiceuses the injectedMessagePublisherto publish aTicketEventto a message broker (e.g., RabbitMQ). This decouples the core CRUD operation from any subsequent asynchronous processing, following an event-driven pattern. - Error Handling: The service throws
RuntimeException(or more specific custom exceptions) when entities are not found, which are then handled by a global exception handler to produce appropriate HTTP error responses.
Code Snippet: Update Logic and Event Publishing
java
// src/main/java/com/slalom/demo/ticketing/service/TicketService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class TicketService {
private final TicketRepository ticketRepository;
private final MessagePublisher messagePublisher;
@Transactional
public TicketResponse updateTicket(Long id, TicketRequest request) {
log.info("Updating ticket with ID: {}", id);
// 1. Retrieve the existing entity or throw an exception
Ticket ticket = ticketRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Ticket not found with id: " + id));
// 2. Map fields from the DTO to the entity
ticket.setTitle(request.getTitle());
ticket.setDescription(request.getDescription());
// ... other fields
// 3. Enforce a specific business rule
if ((request.getStatus() == TicketStatus.RESOLVED || request.getStatus() == TicketStatus.CLOSED)
&& ticket.getResolvedAt() == null) {
ticket.setResolvedAt(LocalDateTime.now());
}
// 4. Save the updated entity (within the transaction)
Ticket updatedTicket = ticketRepository.save(ticket);
log.info("Ticket updated: {}", updatedTicket.getId());
// 5. Publish an event for downstream consumers
publishTicketEvent(updatedTicket, "UPDATED");
// 6. Map the final entity state to a response DTO
return toResponse(updatedTicket);
}
private void publishTicketEvent(Ticket ticket, String eventType) {
TicketEvent event = TicketEvent.builder()
.eventType(eventType)
.ticketId(ticket.getId())
// ... build event payload
.build();
messagePublisher.publishTicketEvent(event);
}
// ... other methods
}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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Repositories
The repository layer is responsible for all data access and persistence operations. It provides an abstraction over the underlying data store (MySQL) and is built using Spring Data JPA.
TicketRepository
The TicketRepository (com.slalom.demo.ticketing.repository.TicketRepository) is an interface that defines the data access methods for the Ticket entity.
Implementation Details:
- Spring Data JPA: The interface extends
JpaRepository<Ticket, Long>. This provides a rich set of standard CRUD methods out-of-the-box, such assave(),findById(),findAll(), anddeleteById(). Spring Data automatically provides the implementation at runtime. - Query Derivation: The repository includes custom query methods whose implementations are derived by Spring Data JPA from their names. This is a powerful feature that avoids the need to write explicit JPQL or SQL for common queries.
List<Ticket> findByStatus(TicketStatus status);List<Ticket> findByRequesterEmail(String requesterEmail);List<Ticket> findByAssignedTo(String assignedTo);
- Abstraction: By using the repository pattern, the service layer is completely decoupled from the specifics of the persistence mechanism (JPA, Hibernate, JDBC). This makes the code cleaner and easier to test, and would simplify a future migration to a different type of database if needed.
Code Snippet: Repository Interface
java
// src/main/java/com/slalom/demo/ticketing/repository/TicketRepository.java
package com.slalom.demo.ticketing.repository;
import com.slalom.demo.ticketing.model.Ticket;
import com.slalom.demo.ticketing.model.TicketStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TicketRepository extends JpaRepository<Ticket, Long> {
// Spring Data JPA will automatically generate a query equivalent to:
// "SELECT t FROM Ticket t WHERE t.status = :status"
List<Ticket> findByStatus(TicketStatus status);
// Query for tickets based on the requester's email address
List<Ticket> findByRequesterEmail(String requesterEmail);
// Query for tickets assigned to a specific user/agent
List<Ticket> findByAssignedTo(String assignedTo);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DTOs (Data Transfer Objects)
DTOs are used to define the "shape" of data sent to and from the API. They are critical for creating a stable, well-defined API contract and for decoupling the external interface from the internal domain model.
Rationale for Separation:
- API Contract Stability: The domain model (
Ticketentity) may change for internal reasons (e.g., adding new fields for internal tracking). Using DTOs ensures that these internal changes do not break the public API contract. - Security: DTOs prevent accidental exposure of sensitive or internal-only entity fields. For example, a
Userentity might have apasswordHashfield that should never be included in an API response. - Flexibility: Allows the API to present data in a format that is most convenient for the client, which may be different from how it is stored in the database.
TicketRequest
The TicketRequest DTO (com.slalom.demo.ticketing.dto.TicketRequest) models the payload for creating and updating tickets. Its primary feature is the use of jakarta.validation annotations to declare constraints on the input data.
java
// src/main/java/com/slalom/demo/ticketing/dto/TicketRequest.java
@Data // Lombok annotation for getters, setters, toString, etc.
@NoArgsConstructor
@AllArgsConstructor
@Builder
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;
private String assignedTo; // Optional field, no validation
}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
TicketResponse
The TicketResponse DTO (com.slalom.demo.ticketing.dto.TicketResponse) defines the structure of a ticket as it is returned by the API. It includes fields that are generated by the system, such as the id and timestamps.
java
// src/main/java/com/slalom/demo/ticketing/dto/TicketResponse.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TicketResponse {
private Long id;
private String title;
private String description;
private TicketStatus status;
private TicketPriority priority;
private String requesterEmail;
private String assignedTo;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime resolvedAt;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19