NOTE

The core difference between .orElse() and .orElseGet() comes down to Performance and Evaluation - Eager vs. Lazy

  • orElse(…)
    • evaluates EAGERLY
    • The default object inside the parentheses is always instantiated, even if the Optional contains a value and the default is never used.
  • orElseGet(…)
    • evaluates LAZILY
    • The lambda expression/supplier is executed only if the Optional is empty.

Direct Technical Comparison

FeatureorElse(...)orElseGet(...)
Input TypeA raw object instance (T)A functional Supplier (Supplier<? extends T>)
Evaluation TimeAlways executed immediately (Eager)Executed only if the container is empty (Lazy)
Memory/CPU OverheadHigh if creating the fallback object is heavyZero if the Optional already has a value

Real-World Production Examples

Scenario 1

NOTE

Use orElse() when the fallback is a constant or free to create==

If your fallback value is a hardcoded string, a primitive, or a pre-existing static object, use orElse(). Creating these takes negligible CPU cycles, so eager evaluation doesn’t hurt performance.

// PRODUCTION EXAMPLE: HTTP Header Check
public String getTrackingId(HttpServletRequest request) {
    return Optional.ofNullable(request.getHeader("X-Tracking-ID"))
                   .orElse("NOT_SET"); // 🟢 Good: "NOT_SET" is a cheap string literal. 
}

Scenario 2

NOTE

Use orElseGet() when the fallback requires a Heavy Operation

If your default value requires allocating a complex object, making a database query, or calling an external service, never use orElse().

The Anti-Pattern (orElse in Production)

// DO NOT DO THIS
public User getUserSettings(String userId) {
    return userRepository.findById(userId)
                         .orElse(fallbackDatabaseService.fetchGlobalSystemDefaults()); 
}
  • This breaks production. How ?
    • Even if userRepository.findById(userId) successfully finds a user 99% of the time, your application will still execute fetchGlobalSystemDefaults() and call the database every single time this method runs. This causes massive, unnecessary database traffic.

The Fix (orElseGet in Production)

// DO THIS INSTEAD
public User getUserSettings(String userId) {
    return userRepository.findById(userId)
                         .orElseGet(() -> fallbackDatabaseService.fetchGlobalSystemDefaults()); 
}
  • Why this is safe
    • The database query inside orElseGet is wrapped inside a lazy lambda. It will only trigger if the user isn’t found in the first repository.

Applying this to a simple StringBuilder Code

In your specific snippet, the difference manifests like this:

// 1. Eager: A new StringBuilder memory allocation occurs EVERY time this line executes
StringBuilder sb2 = sbo.orElse(new StringBuilder("Not Present"));
 
// 2. Lazy: A new StringBuilder allocation occurs ONLY if sbo is empty
StringBuilder sb3 = sbo.orElseGet(() -> new StringBuilder("Not Present"));

If this code executes inside a tight loop processing millions of transactions, orElse will create millions of garbage StringBuilder instances that immediately need to be cleaned up by the Garbage Collector. orElseGet avoids this entire memory overhead.

Fallback patterns with Optional

Best practices to throw custom API responses, or initializing default configurations/beans on startup

Approach 1: Throwing Custom API Responses

In a modern production application (like Spring Boot), you rarely want to return a fallback object like "Not Present" directly to the client.

Instead, if data is missing, - you should interrupt execution and throw a distinct, meaningful exception that an automated global exception handler can catch and convert into a clean HTTP status code (like 404 Not Found).

Production Implementation - .orElseThrow

  • Instead of orElse or orElseGet, use .orElseThrow().
  • This uses a lazy evaluation supplier pattern to ensure the exception object is only created if data is actually missing.
@RestController
@RequestMapping("/api/users")
public class UserController {
 
    @Autowired
    private UserRepository userRepository;
 
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUserById(@PathVariable String id) {
        User user = userRepository.findById(id)
            // 🟢 BEST PRACTICE: Throws lazily only if the user is missing
            .orElseThrow(() -> new ResourceNotFoundException("User with ID " + id + " not found"));
 
        return ResponseEntity.ok(convertToDto(user));
    }
}

Where to throw exceptions ? in controller or in the service layer ?

private Account getAccountOrThrow(String accountId) {  

return accountRepository

.findById(accountId)  
.orElseThrow(() -> new AccountNotFoundException("Account not found: " + accountId));  

}

Standard is to throw it in service layer and sometimes in controller, if needed. Check [[1. Exception handling in Rest]]

Supporting Global Exception Handler

To prevent raw stack traces from leaking to the frontend, map your custom exception to a clean API response using an @ControllerAdvice component:

@ControllerAdvice
public class GlobalExceptionHandler {
 
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorDetails> handleNotFound(ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(
            LocalDateTime.now(), 
            ex.getMessage(), 
            request.getDescription(false)
        );
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); // 404 Status
    }
}

Approach 2: Initializing Default Configurations or Beans

When configuring your application on startup, you often look for

Environment variables or External configuration values

If they aren’t provided, you fall back to a safe default config

Production Implementation

  • For system configurations, .orElse() is often preferred if your default fallback is a pre-allocated constant or a lightweight placeholder.
  • However, if building the fallback setup involves parsing files or initializing network blocks, use .orElseGet().
@Configuration
public class ThirdPartyApiConfig {
 
    // Pre-allocated static fallback configuration bean (Thread-Safe & Immutable)
    private static final ApiClientSettings DEFAULT_SETTINGS = new ApiClientSettings("https://company.com", 5000);
 
    @Bean
    public ApiClient apiClient(Environment env) {
        return Optional.ofNullable(env.getProperty("api.custom.url"))
            // Transform the string URL into a fully operational ApiClient if present
            .map(customUrl -> new ApiClient(customUrl, 10000)) 
            
            // 🟢 BEST PRACTICE: Use orElse() because DEFAULT_SETTINGS is a pre-allocated static constant.
            // No new memory allocations occur here on lookup failure.
            .orElse(DEFAULT_SETTINGS); 
    }
}

What if the default configuration requires a database check?

If your default configuration isn’t a hardcoded constant and must be fetched from an external source dynamically, pivot immediately to .orElseGet() to prevent loading it on every bean verification cycle:

@Bean
public AppConfig dynamicConfig(ConfigRepository repo) {
    return Optional.ofNullable(System.getenv("APP_ENV_CONFIG"))
        .map(json -> parseConfigJson(json))
        // 🟢 BEST PRACTICE: Only queries the DB if the environment variable is blank
        .orElseGet(() -> repo.findActiveSystemDefault()); 
}

Summary

Rule of Thumb

Use Optional.map() and Optional.flatMap() to securely transform data fields before applying these fallback rules