✅ Feature 2: Reliability Improvements (Implemented)
Purpose: Fix unsafe patterns that cause runtime errors and data inconsistency.
Status: ✅ COMPLETE (2026-01-06) Priority: CRITICAL (prevents runtime failures) Estimated Time: 2 days Dependencies: None (but recommended after Feature 1)
Overview
This feature fixes 4 critical reliability issues that cause production crashes:
- ✅ Unsafe
Optional.get()usage → Runtime exceptions - ✅ Missing
@Transactional→ Data inconsistency - ✅ Generic exception catching → Hidden bugs
- ✅ Missing null checks → NullPointerExceptions
Step 1: Fix Unsafe Optional.get() Usage
Problem: Optional.get() throws NoSuchElementException if empty.
File: server/src/main/java/com/saas/springular/user/api/AuthenticationServiceImpl.java
Current Code (UNSAFE):
var user = userRepository.findById(userPrincipal.getId()).get(); // ❌ CRASHES if not found
Fixed Code (SAFE):
var user = userRepository.findById(userPrincipal.getId())
.orElseThrow(() -> new UserOperationException(
"User not found with id: " + userPrincipal.getId(),
HttpStatus.NOT_FOUND
));
Find All Unsafe Optional Usage
Search command:
cd server
grep -rn "\.get()" src/main/java --include="*.java" | grep Optional
Common patterns to fix:
// ❌ UNSAFE - throws NoSuchElementException
Optional<User> userOpt = userRepository.findById(id);
User user = userOpt.get();
// ✅ SAFE - throws custom exception with context
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
// ❌ UNSAFE
User user = userRepository.findByEmail(email).get();
// ✅ SAFE
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException("User not found with email: " + email));
Create Custom Exception Classes
File: server/src/main/java/com/saas/springular/common/exception/UserNotFoundException.java (NEW)
package com.saas.springular.common.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
File: server/src/main/java/com/saas/springular/common/exception/ResourceNotFoundException.java (NEW)
package com.saas.springular.common.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private final String resourceName;
private final String fieldName;
private final Object fieldValue;
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
}
Update All Services
AuthenticationServiceImpl.java:
@Service
@AllArgsConstructor
public class AuthenticationServiceImpl implements AuthenticationService {
@Override
public JwtResponse getCurrentUser(UserPrincipal userPrincipal) {
var user = userRepository.findById(userPrincipal.getId())
.orElseThrow(() -> new UserNotFoundException(
"User not found with id: " + userPrincipal.getId()
));
// Continue with user...
}
@Override
public void resetPassword(String token, String newPassword) {
var resetToken = passwordResetTokenRepository.findByToken(token)
.orElseThrow(() -> new InvalidTokenException("Invalid password reset token"));
var user = userRepository.findById(resetToken.getUserId())
.orElseThrow(() -> new UserNotFoundException(
"User not found with id: " + resetToken.getUserId()
));
// Continue...
}
}
Stripe Service (if applicable):
public Invoice getInvoice(String invoiceId) {
return stripeInvoiceRepository.findById(invoiceId)
.orElseThrow(() -> new ResourceNotFoundException(
"Invoice", "id", invoiceId
));
}
Step 2: Add @Transactional Annotations
Problem: Without transactions, partial updates can occur on errors, causing data inconsistency.
Add to Service Layer
File: server/src/main/java/com/saas/springular/user/api/AuthenticationServiceImpl.java
Before (NO TRANSACTION):
@Override
public void registerUser(SignupRequest signupRequest) {
// Multiple database operations without transaction
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new IllegalArgumentException("Email already in use");
}
User user = new User();
user.setEmail(signupRequest.getEmail());
user.setPassword(passwordEncoder.encode(signupRequest.getPassword()));
userRepository.save(user); // ❌ If this fails, no rollback
// Create default role
roleRepository.save(new Role(user.getId(), "USER")); // ❌ Could be saved even if user save fails
}
After (WITH TRANSACTION):
@Override
@Transactional // ✅ All operations in single transaction
public void registerUser(SignupRequest signupRequest) {
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new IllegalArgumentException("Email already in use");
}
User user = new User();
user.setEmail(signupRequest.getEmail());
user.setPassword(passwordEncoder.encode(signupRequest.getPassword()));
userRepository.save(user);
// If this fails, user save is rolled back
roleRepository.save(new Role(user.getId(), "USER"));
}
Transaction Best Practices
State-changing operations need @Transactional:
@Service
@RequiredArgsConstructor
public class UserService {
@Transactional // ✅ Creates/updates data
public User createUser(UserRequest request) { }
@Transactional // ✅ Updates data
public User updateUser(Long id, UserRequest request) { }
@Transactional // ✅ Deletes data
public void deleteUser(Long id) { }
@Transactional(readOnly = true) // ✅ Read-only transaction (performance optimization)
public User getUser(Long id) { }
@Transactional(readOnly = true) // ✅ Read-only
public List<User> getAllUsers() { }
}
Add to All Services
AuthenticationServiceImpl.java:
@Service
@RequiredArgsConstructor
public class AuthenticationServiceImpl implements AuthenticationService {
@Transactional(readOnly = true)
public JwtResponse getCurrentUser(UserPrincipal userPrincipal) {
// Read-only operation
}
@Transactional
public void registerUser(SignupRequest signupRequest) {
// Creates user + roles
}
@Transactional
public void resetPassword(String token, String newPassword) {
// Updates user password + deletes token
}
}
Stripe/Payment Service:
@Transactional
public Subscription createSubscription(Long userId, String priceId) {
// Multiple operations that must succeed/fail together
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
// Create Stripe subscription
Subscription subscription = stripeService.createSubscription(user.getStripeCustomerId(), priceId);
// Save subscription record
subscriptionRepository.save(subscription);
// Update user status
user.setSubscriptionStatus(SubscriptionStatus.ACTIVE);
userRepository.save(user);
return subscription;
}
Step 3: Improve Exception Handling
Problem: Generic catch (Exception e) hides bugs and makes debugging difficult.
Replace Generic Exception Handling
File: server/src/main/java/com/saas/springular/common/stripe/webhook/StripeWebhookController.java
Before (GENERIC):
@PostMapping("/webhooks")
public ResponseEntity<String> handleWebhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String signature) {
try {
Event event = webhookService.constructEvent(payload, signature);
handleEvent(event);
return ResponseEntity.ok("Success");
} catch (Exception e) { // ❌ Catches everything, including programming errors
log.error("Error handling Stripe event: {}", event.getType(), e);
return ResponseEntity.internalServerError().build();
}
}
After (SPECIFIC):
@PostMapping("/webhooks")
public ResponseEntity<String> handleWebhook(
@RequestBody String payload,
@RequestHeader("Stripe-Signature") String signature
) {
try {
Event event = webhookService.constructEvent(payload, signature);
handleEvent(event);
return ResponseEntity.ok("Success");
} catch (SignatureVerificationException e) {
log.error("Invalid Stripe signature", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid signature");
} catch (StripeException e) {
log.error("Stripe API error: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Stripe error");
} catch (IllegalStateException e) {
log.error("Invalid webhook state: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid state");
}
// Let framework handle other exceptions (programming errors should propagate)
}
Exception Handling Patterns
Good Pattern (specific exceptions):
try {
processPayment(order);
} catch (InsufficientFundsException e) {
log.warn("Payment failed - insufficient funds: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).body(e.getMessage());
} catch (PaymentProviderException e) {
log.error("Payment provider error", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Payment service unavailable");
} catch (InvalidCardException e) {
log.warn("Invalid card: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
Bad Pattern (generic catch-all):
try {
processPayment(order);
} catch (Exception e) { // ❌ Hides all errors
log.error("Payment error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
Update Global Exception Handler
File: server/src/main/java/com/saas/springular/common/exception/GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(
UserNotFoundException ex,
HttpServletRequest request
) {
ErrorResponse error = new ErrorResponse(
ex.getMessage(),
HttpStatus.NOT_FOUND.value(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorResponse> handleBadCredentials(
BadCredentialsException ex,
HttpServletRequest request
) {
log.warn("Authentication failed: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
"Invalid credentials",
HttpStatus.UNAUTHORIZED.value(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse error = new ErrorResponse(
"Validation failed: " + String.join(", ", errors),
HttpStatus.BAD_REQUEST.value(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(JwtException.class)
public ResponseEntity<ErrorResponse> handleJwtException(
JwtException ex,
HttpServletRequest request
) {
log.error("JWT error: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
"Invalid authentication token",
HttpStatus.UNAUTHORIZED.value(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
// Still catch unexpected errors, but log them properly
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedErrors(
Exception ex,
HttpServletRequest request
) {
log.error("Unexpected error", ex); // Full stack trace for debugging
ErrorResponse error = new ErrorResponse(
"An unexpected error occurred",
HttpStatus.INTERNAL_SERVER_ERROR.value(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Step 4: Add Null Checks
Problem: Dereferencing null objects causes NullPointerException.
Add Null Checks in Critical Paths
Stripe Webhook Handler:
private void handleEvent(Event event) {
StripeObject stripeObject = event.getDataObjectDeserializer()
.getObject()
.orElse(null);
if (stripeObject == null) { // ✅ Check before dereferencing
log.error("Stripe event has no data object: {}", event.getId());
throw new IllegalStateException("Invalid Stripe event - no data object");
}
// Safe to use stripeObject now
if (stripeObject instanceof Subscription subscription) {
handleSubscriptionEvent(event.getType(), subscription);
}
}
JWT Token Extraction:
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken == null) { // ✅ Check for null
return null;
}
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
Use @NonNull Annotation
Add to method parameters:
import lombok.NonNull;
public User updateUser(@NonNull Long id, @NonNull UserRequest request) {
// Lombok generates null checks at compile time
}
Use Objects.requireNonNull
For critical validations:
import java.util.Objects;
public void processPayment(Order order) {
Objects.requireNonNull(order, "Order cannot be null");
Objects.requireNonNull(order.getCustomerId(), "Customer ID cannot be null");
Objects.requireNonNull(order.getAmount(), "Amount cannot be null");
// Safe to proceed
}
Verification
Test Optional Safety
Create test:
@Test
void getCurrentUser_UserNotFound_ThrowsException() {
UserPrincipal principal = new UserPrincipal(999L, "test@test.com", null);
when(userRepository.findById(999L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> authService.getCurrentUser(principal))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("User not found");
}
Test Transaction Rollback
Create test:
@Test
@Transactional
void registerUser_RoleCreationFails_RollsBackUserCreation() {
SignupRequest request = new SignupRequest();
request.setEmail("test@test.com");
request.setPassword("Test123!@#");
// Make role creation fail
doThrow(new RuntimeException("Role creation failed"))
.when(roleRepository).save(any());
assertThatThrownBy(() -> authService.registerUser(request))
.isInstanceOf(RuntimeException.class);
// Verify user was NOT saved (rolled back)
assertThat(userRepository.existsByEmail("test@test.com")).isFalse();
}
Test Exception Handling
Test specific exception:
# Should return specific error message, not generic 500
curl -X POST http://localhost:8081/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"nonexistent@test.com","password":"Test123"}'
# Expected: 401 Unauthorized with "Invalid credentials" message
Common Patterns Fixed
Pattern 1: Repository Calls
Before:
User user = userRepository.findById(id).get(); // ❌
After:
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id)); // ✅
Pattern 2: Service Methods
Before:
public void updateUser(Long id, UserRequest request) { // ❌ No transaction
User user = userRepository.findById(id).get(); // ❌ Unsafe
user.setName(request.getName());
userRepository.save(user);
}
After:
@Transactional // ✅ Transaction
public void updateUser(Long id, UserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id)); // ✅ Safe
user.setName(request.getName());
userRepository.save(user);
}
Pattern 3: Exception Handling
Before:
try {
stripeService.charge(amount);
} catch (Exception e) { // ❌ Generic
log.error("Error", e);
}
After:
try {
stripeService.charge(amount);
} catch (StripeException e) { // ✅ Specific
log.error("Stripe API error: {}", e.getMessage(), e);
throw new PaymentProcessingException("Payment failed", e);
}
Summary
Reliability Issues Fixed:
- ✅ Unsafe Optional.get() → orElseThrow() with custom exceptions
- ✅ No transactions → @Transactional on all state-changing methods
- ✅ Generic exceptions → Specific exception handling
- ✅ Missing null checks → Validation before dereferencing
Benefits:
- No more
NoSuchElementExceptioncrashes - Data consistency guaranteed by transactions
- Better error messages for debugging
- Fewer
NullPointerExceptionerrors