1

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?

1
  • 1
    Indirection and layers are tools to convert complexity and aid flexibilty. I write convert because while overall complexity may rise, local complexity is reduced. Your approach is locally more complex (i have to think about the api boundary) and less flexible (what if that boundary Changes). But it may also be fine: this is where you apply thinking (& using context) instead of following guidelines blindly. Commented Nov 10 at 11:41

4 Answers 4

3

Let me change your question a little bit, as it could be general instead of Spring based.

Is it good practice to return an API Response from a service layer?

There is going to be multiple comments here, nothing wrong about it since there are multiple ways of implementing a single solution.

However, from my perspective and taking good practices if you are doing a layer-based application there is an important rule to follow: each layer has its own responsibility and shouldn't carry the responsibility of another layer.

  1. Service layers are where usually you perform the BL (Business Logic), validations, work with pure entities instead of DTOs and return pure entities too, is not responsible of returning API responses or transforming DTOs, that's why controller exists.

  2. Controller layers are responsible of letting the clients know the DTOs, server responses (e.g operation results).

There's usually a confusion when handling service exceptions, it is really easy to question the following: If the service layer is not responsible of knowing the server responses, what happens with throwing exceptions in there?

I consider there's nothing wrong about it. Indeed, you need to have a way in order to throw exceptions when something happens. One thing is throwing exceptions and the other one throwing the server operation result.

Let's do a comparison table:

Approach Pros Cons
Service returns ApiResponse Simple, fast to implement, keeps controller thin Couples business logic to HTTP layer, harder to reuse or test service in non-HTTP contexts
Service returns domain object / throws exception Clean separation, reusable logic, easier testing Requires slightly more boilerplate or global exception handling
1

Is it good practice to return ApiResponse from a Spring service layer?

David Parnas proposed about modularization, and in particular the benefits of choosing your module boundaries so that the modules "hide" difficult design decisions or design decisions that are likely to change.

For design decisions that are NOT likely to change, separating the modules may not be so important.

Spring is plumbing - it's a way for the outside world to communicate with your voucher logic, but it certainly isn't the only way. If clean designs were free, we would certainly prefer a design that allows us to change the plumbing cheaply to a design that locks us in to a particular "vendor".

But if the decision to use spring is stable, and if the ApiResponse interface is stable, and so on, then there are probably other areas where you can invest effort to get better returns.


That said, if you did want to separate your voucher logic from the plumbing, it might go something like this.

First, we'd probably do a little refactoring to isolate the part of the code that is coupled to the ApiResponse. That might look like:

public ApiResponse<VoucherDto> claimVoucher(Long userId, Long templateId) { ... return returnValue(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)"); } } // TODO: improve this name ApiResponse<VoucherDto> returnValue(Voucher saved) { return ApiResponse.ok(toVoucherDto(saved)); } 

With that idea in mind, we might then extract the returnValue function into a separate object

public ApiResponse<VoucherDto> claimVoucher(Long userId, Long templateId) { ... return this.someFactory.returnValue(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)"); } } // TODO: improve this name interface SomeFactory { // TODO: improve this name ApiResponse<VoucherDto> returnValue(Voucher saved); } 

And then comes the magic trick:

public T claimVoucher(Long userId, Long templateId) { ... } // TODO: improve this name interface SomeFactory<T> { // TODO: improve this name T returnValue(Voucher saved); } 

So instead of having your voucher logic tightly coupled to ApiResponse, you have voucher logic that is tightly coupled to an interface, where one implementation of that interface is tightly coupled to the ApiResponse.

Complexity has increased (more parts to think about), but the responsibilities of the parts are more focused, and you have more freedom to assemble the parts in different ways.

Of course, you'd want to do something similar with the errors being returned

public T claimVoucher(Long userId, Long templateId) { ... return this.someFactory.returnValue(saved); } catch (DataIntegrityViolationException e) { return someFactory.returnValue(e); } catch (OptimisticLockException e) { return someFactory.returnValue(e); } } // TODO: improve this name interface SomeFactory<T> { // TODO: improve this name T returnValue(Voucher saved); T returnValue(DataIntegrityViolationException e); T returnValue(OptimisticLockException e); } 

In the long term, you get the ability to change the details of the plumbing without impacting your voucher logic. In the short term, you maybe get better affordances for testing: the calculation of returned values can be tested independently of the branching logic, and the branching logic can be tested more flexibly, and without being tightly coupled to Spring.

Against this, you've split one thing into two -- there are now both more nodes in your code graph, and more edges between nodes; the cost of that may be small enough that you don't care, but it isn't free.

Surprise: it's trade offs all the way down.

0

From a helicopter view, since it is about a web application and HTTP protocol it's going to get used to send some data transfer object (DTO) from the server to the client, ApiResponse it is redundant: HTTP status code 200 OK, API success true OK. Hyper text transfer protocol (HTTP) includes information ApiResponse it's going to add and that increases network load/consumes bandwidth without benefits or at least without obvious benefits. Either HTTP is going to be avoided and then ApiResponse is HTTP minimum viable replacement, either HTTP is going to be used and ApiResponse should be avoided.

1
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center. Commented Nov 13 at 2:21
0

To be short, the answer is clearly no, but I will use many approach to argument this answer:

  • programmatic and pragmatic approach:
    You are using Spring ApiResponse, and what if you use @Controller and ResponseEntity? and what if you use @RestController instead?
    With " @Controller" annotation in spring you use a responseEntity, another class response, and you will have to recode your service. (Side note, I don't know what Spring module you use for ApiResponse, but that's not the main problem here).
    RestControllers doesn't use ApiResponse att all, and you will end up either unwrap the ApiResponse or your service, or modify all you service to fit your controller signature. Or even worse duplicate your service for every possible usage.

Basically, your controller and service layers are heavily coupled: You can only use the service for the designed controller, your service is supposed to be reusable, but you can't reuse your service because you have a responsibility problem, which make a transition to the next point

  • SOLID Approach: Your service has one responsibility, SRP, it is business code. think that it could be called from a command line, another service, where all the HTTP information is useless. the controller manage the HTTP information, the service the only the business implementation, with no knowledge of the caller context.

  • Hexagonal approach: which form my point of vue is an extreem approach to implements SOLID:
    Your service should only manage standard java, and not rely on Spring at all. If you are more pragmatic like me, you can use Spring injection (service/autowire) but that's all, no HTTP management, and even no database manipulation.
    So no code from Spring Web should be in the service Layer.

Whatever the code architecture or clean code habit you choose, using hardcoded class bound to a specific stack of your framework in the service will be a nightmare for maintenance and simply a useless constraint in development phase.

From your example, I see that you manage errors using HTTP code, the correct way to do it is to use java exception for errors, and let the normal return type "VoucherDto" without any wrapper. There are many mecanism in Spring to dispatch exception on the correct HTTP code.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.