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?