Why the Service Layer Should Not Return a Result Object in Java
The article explains why returning a Result wrapper from the Service layer couples business and presentation logic, reduces reusability, complicates testing and transaction handling, and suggests keeping services pure by returning domain objects and handling errors via exceptions.
Introduction
During a code review the author noticed a colleague returning a Result<User> directly from the Service layer. This sparked a technical discussion about the impact on layering, responsibility separation, and code reuse.
Responsibility Separation Principle
In a traditional MVC architecture, the Service layer handles business logic while the Controller deals with HTTP request processing and response formatting. When a Service returns a Result, it mixes business processing with response handling, leading to tighter coupling and reduced code clarity.
@Service
public class UserService {
public Result<User> getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
return Result.error(404, "User not found");
}
return Result.success(user);
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}This approach forces every Service method to also manage response formatting and error codes.
Reusability Issues
When Service methods return Result, other services that depend on them must unpack the wrapper, adding boilerplate and obscuring the actual data.
@Service
public class OrderService {
@Autowired
private UserService userService;
// Not recommended: need to unpack Result
public void createOrder(Long userId, OrderDTO orderDTO) {
Result<User> userResult = userService.getUserById(userId);
if (!userResult.isSuccess()) {
throw new BusinessException(userResult.getMessage());
}
User user = userResult.getData();
// further business logic
}
}Two main problems arise: developers must understand the Result structure, and they need extra checks for success, increasing complexity.
Exception Handling Mechanism
Returning Result often leads to repetitive error handling code. By throwing exceptions and using a global exception handler, error handling becomes centralized and the Service layer stays focused on business logic.
public void createOrder(Long userId, OrderDTO orderDTO) {
if (userId == null) {
throw new BusinessException("User ID cannot be null");
}
// business logic
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(400, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("System error", e);
return Result.error(500, "System busy");
}
}This reduces duplicated error handling and keeps business code clean.
Testing Convenience
When Services return plain domain objects, unit tests can directly assert on the returned data without dealing with the wrapper.
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetUserById() {
User user = userService.getUserById(1L);
assertNotNull(user);
assertEquals("张三", user.getName());
}
@Test
public void testGetUserById_NotFound() {
assertThrows(BusinessException.class, () -> userService.getUserById(999L));
}
}With a Result wrapper, tests become longer and need to check the wrapper's status and data.
Domain‑Driven Design Perspective
From a DDD viewpoint, the Service layer belongs to the application or domain layer and should expose domain concepts. Result is an infrastructure concern (HTTP response) and should not pollute the domain model.
@Service
public class TransferService {
public TransferResult transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account from = accountRepository.findById(fromAccountId);
Account to = accountRepository.findById(toAccountId);
from.deduct(amount);
to.deposit(amount);
accountRepository.save(from);
accountRepository.save(to);
return new TransferResult(from, to, amount);
}
}The TransferResult is a domain object, not a generic HTTP wrapper.
Interface Adaptability
When Services return pure domain objects, Controllers can adapt the response format to different protocols (REST, GraphQL, RPC) without changing the Service code.
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// REST returns Result
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}
// GraphQL returns domain object directly
@QueryMapping
public User user(@Argument Long id) {
return userService.getUserById(id);
}
}This keeps the Service reusable across different interfaces.
Clear Transaction Boundaries
Service methods are typical transaction boundaries. Returning a domain object makes it clear that a successful return commits the transaction, while throwing an exception triggers a rollback.
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderDTO orderDTO) {
Order order = new Order();
orderMapper.insert(order);
inventoryService.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
return order;
}
}If the Service returns a Result, the transaction may not roll back on logical failures, leading to data inconsistency.
Conclusion
Keeping the Service layer focused on pure business logic—returning domain objects and using exceptions for error cases—preserves separation of concerns, improves reusability, simplifies testing, aligns with DDD, enables flexible interface adaptation, and ensures clear transaction semantics.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
