Spring Boot Result Starter
A Spring Boot library that implements the Result pattern for elegant error handling and response management.
Installation
Maven
<dependency>
<groupId>io.github.homitra</groupId>
<artifactId>spring-boot-starter-result</artifactId>
<version>0.0.9</version>
</dependency>
Gradle
implementation 'io.github.homitra:spring-boot-starter-result:0.0.9'
The Problem with Traditional Exception Handling
Traditional Spring Boot error handling has several issues:
// Traditional approach - Problems:
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
try {
User user = userService.findById(id); // Might throw!
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (ValidationException e) {
return ResponseEntity.badRequest().build();
}
// What if we forget to handle an exception? 💥
}
Issues:
- ❌ Unpredictable Control Flow - Exceptions jump out unexpectedly
- ❌ Hidden Failure Points - No way to know what exceptions a method throws
- ❌ Forgotten Error Handling - Easy to miss catch blocks
- ❌ Inconsistent Responses - Different error formats across API
- ❌ Performance Impact - Exception stack traces are expensive
- ❌ Testing Complexity - Hard to test all error scenarios
What is the Result Pattern?
The Result pattern makes success and failure explicit in your code. Instead of throwing exceptions, methods return a Result<T> object that either contains:
- Success: The expected data of type
T - Failure: Error information with details about what went wrong
// Result pattern - Clean and predictable:
@GetMapping("/users/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
Result<User> result = userService.findById(id);
return ResponseUtils.asResponse(result);
// That's it! All errors handled automatically ✨
}
Why This is Better
✅ Explicit Error Handling - Success and failure are part of the method signature
✅ Predictable Control Flow - No unexpected jumps in execution
✅ Type Safety - Compiler ensures you handle both success and failure cases
✅ Consistent API Responses - Uniform response format across your application
✅ Better Performance - No expensive exception stack traces
✅ Easier Testing - Test success and failure scenarios with simple assertions
✅ Self-Documenting Code - Method signatures tell you exactly what can go wrong
Quick Example
@RestController
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
Result<User> result = userService.findById(id);
return ResponseUtils.asResponse(result);
// Automatically returns 404 if user not found
// Returns 200 with user data if found
}
}
@Service
public class UserService {
public Result<User> findById(Long id) {
return userRepository.findById(id)
.map(Result::success)
.orElse(Result.entityNotFoundError("User not found"));
// Explicit success or failure - no surprises!
}
}
Response Format
All responses follow a consistent structure:
Success Response:
{
"success": true,
"message": "Operation completed successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
}
Error Response:
{
"success": false,
"message": "User not found",
"data": null
}
Key Features
🔒 Type-Safe Error Handling
No more forgotten exception handling. The compiler ensures you handle both success and failure cases.
🌐 Automatic HTTP Integration
Automatic status code mapping:
EntityNotFoundError→ 404 NOT_FOUNDValidationError→ 400 BAD_REQUESTUnauthorizedError→ 401 UNAUTHORIZEDForbiddenError→ 403 FORBIDDENEntityAlreadyExistsError→ 409 CONFLICT
🔄 Smart Transaction Management
@RollbackOnFailure marks the entire transaction for rollback when the method returns a failed Result:
@RollbackOnFailure
public Result<User> createUserWithProfile(CreateUserRequest request) {
// All these operations happen first
User user = userRepository.save(new User(request.getName(), request.getEmail()));
profileService.createProfile(user.getId());
auditService.logUserCreation(user.getId());
// If this validation fails, ALL above operations are rolled back
if (businessRuleViolated(user)) {
return Result.validationError("Business rule violated");
}
return Result.success(user);
}
⚡ Fluent Validation Chains
Result<User> result = Result.success(new User(name, email))
.validate(user -> user.getName() != null, "Name is required")
.validate(user -> user.getEmail().contains("@"), "Invalid email")
.validate(user -> user.getName().length() >= 2, "Name too short");
🚀 Async Support
@GetMapping("/users/{id}/async")
public CompletableFuture<ResponseEntity<?>> getUserAsync(@PathVariable Long id) {
return Result.async(() -> userService.findById(id))
.thenApply(ResponseUtils::asResponse);
}
📢 Event Publishing
@PublishEvent(on = PublishEvent.EventType.SUCCESS)
public Result<User> createUser(CreateUserRequest request) {
// Event published automatically on success
return Result.success(userRepository.save(user));
}
Architecture Benefits
For Developers
- Less Boilerplate - No more try-catch blocks everywhere
- Better IDE Support - Autocomplete shows you all possible outcomes
- Easier Debugging - Clear error messages without stack traces
- Functional Style - Chain operations with
map,flatMap,validate
For Teams
- Consistent Codebase - Everyone handles errors the same way
- Easier Code Reviews - Error handling is explicit and visible
- Better Documentation - Method signatures are self-documenting
- Reduced Bugs - Compiler catches unhandled error cases
For Applications
- Better Performance - No exception overhead
- Consistent APIs - Uniform response format
- Better User Experience - Predictable error messages
- Easier Integration - Clear success/failure indicators
Requirements
- Java: 17+ (Java 21+ recommended for virtual threads)
- Spring Boot: 3.0+
- Spring Framework: 6.0+
Next Steps
Ready to transform your error handling? Check out the Quick Start Guide!