I'm working on a Spring Boot application with a VoucherService and VoucherController. I currently have the service method return an ApiResponse<T> directly, like this:
@Transactional
public ApiResponse<VoucherDto> claimVoucher(Long userId, Long templateId) {
VoucherTemplate template = templateRepository.findById(templateId)
.orElseThrow(() -> new IllegalArgumentException("Template not found"));
LocalDateTime now = TimeUtils.now();
if (!template.isActive() || template.isArchived())
return ApiResponse.error("Voucher template inactive or archived");
if (template.getValidFrom().isAfter(now))
return ApiResponse.error("Voucher template not active yet");
if (template.getValidTo().isBefore(now))
return ApiResponse.error("Voucher template expired");
if (template.getClaimedCount() >= template.getTotalQuantity())
return ApiResponse.error("Voucher is out of stock");
if (voucherRepository.existsByUserIdAndTemplate_Id(userId, templateId))
return ApiResponse.error("You have already claimed this voucher");
try {
template.setClaimedCount(template.getClaimedCount() + 1);
templateRepository.saveAndFlush(template);
Voucher voucher = Voucher.builder()
.template(template)
.userId(userId)
.active(true)
.used(false)
.claimedAt(now)
.validFrom(template.getValidFrom())
.validTo(template.getValidTo())
.build();
Voucher saved = voucherRepository.save(voucher);
return ApiResponse.ok(toVoucherDto(saved));
} catch (DataIntegrityViolationException e) {
return ApiResponse.error("You have already claimed this voucher");
} catch (OptimisticLockException e) {
return ApiResponse.error("Voucher claim failed, please try again (system busy)");
}
}
And in my controller:
@PostMapping("/claim/{templateId}")
@PreAuthorize("hasRole('USER')")
public ApiResponse<VoucherDto> claimVoucher(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable("templateId") Long templateId
) {
return voucherService.claimVoucher(user.getId(), templateId);
}
My ApiResponse class is defined as:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, "OK", data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
}
I know some people prefer that service methods throw exceptions or return domain objects, leaving the controller to wrap responses in a standard API format.
My question:
Is it considered good practice for a service layer to return ApiResponse directly, or should I let the controller handle response wrapping? What are the pros and cons of each approach in a Spring Boot REST API?