Troubleshooting & FAQ
Common issues and solutions when using Spring Boot Result Starter.
Common Issues
1. @RollbackOnFailure Not Working
Problem: Transactions are not rolling back when Result indicates failure.
Symptoms:
- Data is saved even when Result contains error
- No rollback occurs on failed Results
Solutions:
Missing Configuration
// ❌ Missing configuration
@Configuration
public class AppConfig {
// No aspect configuration
}
// ✅ Correct configuration
@Configuration
@EnableAspectJAutoProxy
@EnableTransactionManagement
public class AppConfig {
@Bean
public TransactionRollbackAspect transactionRollbackAspect() {
return new TransactionRollbackAspect();
}
}
Missing @Transactional
// ❌ No transaction context
@Service
public class UserService {
@RollbackOnFailure
public Result<User> createUser(CreateUserRequest request) {
// No transaction to rollback
return Result.success(userRepository.save(user));
}
}
// ✅ With transaction context
@Service
@Transactional
public class UserService {
@RollbackOnFailure
public Result<User> createUser(CreateUserRequest request) {
return Result.success(userRepository.save(user));
}
}
Incorrect Return Type
// ❌ Wrong return type
@RollbackOnFailure
public User createUser(CreateUserRequest request) {
// Aspect only works with Result<T> return types
return userRepository.save(user);
}
// ✅ Correct return type
@RollbackOnFailure
public Result<User> createUser(CreateUserRequest request) {
return Result.success(userRepository.save(user));
}
2. @PublishEvent Not Triggering
Problem: Events are not being published when using @PublishEvent.
Solutions:
Missing Aspect Configuration
// ✅ Add event publishing aspect
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean
public EventPublishingAspect eventPublishingAspect(ApplicationEventPublisher publisher) {
return new EventPublishingAspect(publisher);
}
}
Incorrect Event Type Configuration
// ❌ Event type mismatch
@PublishEvent(on = PublishEvent.EventType.SUCCESS)
public Result<User> createUser(CreateUserRequest request) {
return Result.validationError("Invalid data"); // Returns failure, no event published
}
// ✅ Correct configuration
@PublishEvent(on = PublishEvent.EventType.BOTH) // or FAILURE
public Result<User> createUser(CreateUserRequest request) {
return Result.validationError("Invalid data"); // Event will be published
}
3. Validation Chains Not Working
Problem: Validation chains are not stopping at first failure.
Example:
// ❌ Problem: All validations run even after first failure
Result<User> result = Result.success(user)
.validate(u -> u.getName() != null, "Name required")
.validate(u -> u.getName().length() > 2, "Name too short"); // NPE if name is null!
Solution:
// ✅ Validation chains stop at first failure
Result<User> result = Result.success(user)
.validate(u -> u.getName() != null, "Name required")
.validate(u -> u.getName().length() > 2, "Name too short"); // Safe - won't run if first fails
The validation chain automatically stops at the first failure, so subsequent validations won't execute.
4. Async Operations Not Working
Problem: Result.async() operations are not executing asynchronously.
Solutions:
Missing @EnableAsync
// ✅ Enable async processing
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
}
Incorrect Usage
// ❌ Blocking call
Result<User> result = Result.async(() -> userService.findById(1L)).get(); // Blocks!
// ✅ Non-blocking usage
CompletableFuture<Result<User>> futureResult = Result.async(() -> userService.findById(1L));
futureResult.thenApply(ResponseUtils::asResponse); // Non-blocking
5. HTTP Status Codes Not Mapping Correctly
Problem: Wrong HTTP status codes returned for specific error types.
Check Error Type Usage:
// ❌ Wrong error type
Result<User> result = Result.failure("User not found"); // Returns 500
// ✅ Correct error type
Result<User> result = Result.entityNotFoundError("User not found"); // Returns 404
Verify ResponseUtils Usage:
// ❌ Manual status code (ignores error type)
if (!result.isSuccess()) {
return ResponseEntity.status(500).body(result);
}
// ✅ Automatic status code mapping
return ResponseUtils.asResponse(result);
Frequently Asked Questions
Q: Can I use Result pattern with existing exception-based code?
A: Yes! You can gradually migrate. Use adapter methods:
// Adapter for legacy code
public User findByIdOrThrow(Long id) {
Result<User> result = findById(id);
if (result.isSuccess()) {
return result.getData();
} else {
throw new UserNotFoundException(result.getError().getMessage());
}
}
// New Result-based method
public Result<User> findById(Long id) {
return userRepository.findById(id)
.map(Result::success)
.orElse(Result.entityNotFoundError("User not found"));
}
Q: How do I handle multiple validation errors?
A: The validation chain stops at the first failure by design. For collecting all validation errors:
public Result<User> validateUserWithAllErrors(User user) {
List<String> errors = new ArrayList<>();
if (user.getName() == null) errors.add("Name is required");
if (user.getEmail() == null) errors.add("Email is required");
if (user.getName() != null && user.getName().length() < 2) errors.add("Name too short");
if (!errors.isEmpty()) {
return Result.validationError(String.join(", ", errors));
}
return Result.success(user);
}
Q: Can I customize the response format?
A: Yes, create your own response utility:
public class CustomResponseUtils {
public static <T> ResponseEntity<CustomResponse<T>> asCustomResponse(Result<T> result) {
if (result.isSuccess()) {
return ResponseEntity.ok(new CustomResponse<>(
"OK",
result.getMessage(),
result.getData()
));
} else {
HttpStatus status = mapErrorToStatus(result.getError());
return ResponseEntity.status(status).body(new CustomResponse<>(
"ERROR",
result.getError().getMessage(),
null
));
}
}
}
Q: How do I test async Result operations?
A: Use CompletableFuture testing patterns:
@Test
void testAsyncOperation() throws Exception {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// When
CompletableFuture<Result<User>> future = Result.async(() -> userService.findById(1L));
// Then
Result<User> result = future.get(5, TimeUnit.SECONDS);
assertThat(result.isSuccess()).isTrue();
}
Q: Can I use Result with Spring Data JPA projections?
A: Yes:
public Result<UserProjection> findUserProjection(Long id) {
return userRepository.findProjectionById(id, UserProjection.class)
.map(Result::success)
.orElse(Result.entityNotFoundError("User not found"));
}
Q: How do I handle file upload errors with Result?
A: Create specific error types:
public Result<String> uploadFile(MultipartFile file) {
if (file.isEmpty()) {
return Result.validationError("File is empty");
}
if (file.getSize() > MAX_FILE_SIZE) {
return Result.validationError("File too large");
}
try {
String filename = fileService.save(file);
return Result.success(filename);
} catch (IOException e) {
return Result.failure("File upload failed: " + e.getMessage());
}
}
Q: Can I use Result with Spring Security?
A: Yes, handle authentication/authorization:
@Service
public class SecureUserService {
public Result<User> findById(Long id) {
if (!securityService.hasPermission("READ_USER")) {
return Result.forbiddenError("Access denied");
}
return userRepository.findById(id)
.map(Result::success)
.orElse(Result.entityNotFoundError("User not found"));
}
}
Q: How do I migrate from ResponseEntity to Result?
A: Gradual migration approach:
// Step 1: Keep existing method, add new Result-based method
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserLegacy(@PathVariable Long id) {
// Existing implementation
}
@GetMapping("/v2/users/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
Result<User> result = userService.findById(id);
return ResponseUtils.asResponse(result);
}
// Step 2: Eventually replace legacy endpoint
Performance Considerations
Q: Is Result pattern slower than exceptions?
A: No, Result pattern is typically faster:
// Performance test results (approximate):
// Result pattern: ~50ms for 100,000 operations
// Exception pattern: ~500ms for 100,000 operations
// Result pattern is ~10x faster for error cases
Q: Memory usage comparison?
A: Result pattern uses less memory:
- No stack trace generation
- No exception object creation
- Predictable memory allocation
Debugging Tips
Enable Debug Logging
# application.yml
logging:
level:
io.github.homitra.spring.boot.result: DEBUG
org.springframework.transaction: DEBUG
org.springframework.aop: DEBUG
Common Debug Scenarios
Check if Aspects are Applied
@Component
public class DebugAspect {
@Around("@annotation(rollbackOnFailure)")
public Object debugRollback(ProceedingJoinPoint pjp, RollbackOnFailure rollbackOnFailure) throws Throwable {
System.out.println("RollbackOnFailure aspect applied to: " + pjp.getSignature().getName());
return pjp.proceed();
}
}
Verify Transaction Status
@RollbackOnFailure
public Result<User> createUser(CreateUserRequest request) {
System.out.println("Transaction active: " + TransactionSynchronizationManager.isActualTransactionActive());
System.out.println("Transaction read-only: " + TransactionSynchronizationManager.isCurrentTransactionReadOnly());
return Result.success(userRepository.save(user));
}
Getting Help
Community Resources
- GitHub Issues: Report bugs and feature requests
- Discussions: Ask questions and share experiences
Before Reporting Issues
- Check this troubleshooting guide
- Verify your configuration
- Test with minimal reproduction case
- Include relevant code snippets and error messages
Issue Template
**Environment:**
- Spring Boot version:
- Java version:
- Library version:
**Configuration:**
[Include your configuration classes]
**Expected Behavior:**
[What you expected to happen]
**Actual Behavior:**
[What actually happened]
**Code Sample:**
[Minimal code that reproduces the issue]