0% completed
Optional
On This Page
What Optional Is For
Repository Results
The Methods to Use
orElse Against orElseGet
The Methods and Uses to Avoid
Key Takeaways
Practice Questions
This is the message Java prints when code asks an empty Optional for its value:
java.util.NoSuchElementException: No value present
In the booking service, it would appear if GET /bookings/99 asked for a missing booking and the code called get() on the result. The message names neither the booking nor its id, so whoever reads the log must find the cause alone. The lookup was right to return "nothing"; the code that received it handled "nothing" badly.
Three questions follow: what Optional is for, how Spring Data repositories return it, and which of its methods to use and to avoid.
What Optional Is For
Optional is an object that holds either exactly one value or nothing, and it is used as a return type to say that "no result" is a normal outcome. The Java documentation states that Optional is primarily intended for use as a method return type where there is a clear need to represent "no result". It also says when: where using null instead is likely to cause errors.
Compare two declarations of the same lookup:
Booking findBooking(long id); // may return null, but the type does not say so Optional<Booking> findBooking(long id); // the type says the booking may be missing
With the first, a caller who forgets the null check gets a NullPointerException later, often far from the lookup. With the second, the caller cannot reach the booking without choosing what happens when it is missing. The type moves that decision to the place where the caller still knows the id.
The two possible states print as Optional[...] with the value inside, or as Optional.empty. An empty Optional is an object, not null, so methods can be called on it safely.
Repository Results
Spring Data repositories return Optional from lookups that may find nothing, starting with findById. The Spring Data documentation for CrudRepository says that findById returns the entity with the given id or Optional#empty() if none found. Repository methods that you declare yourself follow the return type you choose.
To see both behaviors, we gave the booking service a CustomerRepository with two lookups by email:
public interface CustomerRepository extends JpaRepository<Customer, Long> { Optional<Customer> findByEmail(String email); Customer readByEmail(String email); }
Spring Data creates the implementation of this interface at startup, and a later lesson shows how. We saved one customer, Ana, ran a test against an in-memory H2 database, and printed each result:
RESULT findByEmail(ana) present: true, role CUSTOMER RESULT findByEmail(nobody): Optional.empty RESULT readByEmail(nobody): null RESULT findById(99): Optional.empty
The two methods ran the same query for an email that does not exist. The one declared with Optional returned Optional.empty, and the one declared with Customer returned null. Spring Data adapts to the declared type, so the declaration is where you decide how absence is reported.
Notice where each result can fail. The empty Optional cannot be used as a customer until the caller handles absence, while null passes silently into the next line and fails there.
The Methods to Use
The useful methods of Optional either supply an answer for the empty case or apply a step only when a value is present. The program below runs each of them against an in-memory BookingRepository that holds one booking: Ana's booking 1 for seat A12.
It prints the following:
1. Two lookups findById(1): Optional[Booking[id=1, seatLabel=A12, email=ana@example.com, status=CONFIRMED]] findById(99): Optional.empty 2. get() on an empty Optional java.util.NoSuchElementException: No value present 3. orElseThrow with an exception that names the problem Booking 99 not found 4. map and filter work only when a value is present seat of booking 1: A12, seat of booking 99: no seat booking 1 for ben@example.com: Optional.empty 5. orElse always runs its argument, orElseGet only when empty orElse, value present: fallbacks built = 1 orElseGet, value present: fallbacks built = 1 6. ifPresentOrElse booking 1: seat A12 booking 99: nothing to cancel
Part 3 is the usual pattern in a service. When a missing value is an error, orElseThrow turns absence into an exception that names the problem:
Booking b = bookings.findById(id) .orElseThrow(() -> new BookingNotFoundException(id));
Compare its message, "Booking 99 not found", with the message of get() in part 2. A later lesson turns such an exception into a 404 Not Found response.
Part 4 shows map and filter. Both run only when a value is present, and on an empty Optional they do nothing. A filter can express an ownership check: booking 1 belongs to Ana, so filtering it for Ben's email gives Optional.empty.
Notice that the empty path skips map entirely. No branch in the code handles booking 99, yet the chain still produces a result.
These are the methods worth knowing:
| Method | Use it when |
|---|---|
orElseThrow(supplier) | A missing value is an error, as in GET /bookings/{id} |
map(function) | You need something derived from the value, such as its seat label |
filter(predicate) | The value counts only if it passes a test, such as ownership |
orElse(value) | The fallback is a ready constant |
orElseGet(supplier) | Building the fallback costs work |
ifPresentOrElse(action, emptyAction) | Each case needs its own action |
orElse Against orElseGet
The argument of orElse is evaluated before the call, even when the Optional holds a value; the supplier of orElseGet runs only when it is empty. Part 5 shows the difference. The method fallbackLabel counts its calls, and orElse built a fallback for booking 1 although the booking existed. The orElseGet call left the counter at 1.
For a constant such as "no seat" the difference costs nothing. It matters when the fallback queries a database, calls another service, or creates a record. In that case, orElse does the work on every call, and it may even create a duplicate record.
The Methods and Uses to Avoid
Optional helps only when it is used as a return type and handled by the caller. Four habits remove that benefit:
- Calling get(). It throws the unhelpful exception from the opening. The documentation names orElseThrow() as the preferred alternative.
- isPresent() followed by get(). This is a null check with more words. Use orElseThrow, map, or ifPresentOrElse instead.
- Optional fields and parameters. The documentation says a variable of type Optional should never itself be null. A parameter of type Optional can still receive null, which gives it three states instead of two.
- Optional of a collection. A method that finds no bookings should return an empty list. An empty list already means "nothing", and callers can loop over it without a check.
Key Takeaways
- Optional holds one value or nothing, and its main use is as a return type where "no result" is normal.
- Spring Data returns Optional from findById, and for your own methods it follows the declared return type: Optional.empty or null.
- Use orElseThrow when absence is an error, so the exception names what was missing.
- map and filter apply only when a value is present, so one chain handles both cases.
- Prefer orElseGet when the fallback costs work, and avoid get(), Optional fields, and Optional collections.
Repository results usually travel onward as small data objects for the API. Records and Enums shows how Java declares such objects in one line, and how an enum carries the status of a booking.
Practice Questions
1. A service method contains Customer c = customers.findByEmail(email).get();. What happens when someone signs in with an unknown email, and how should the line change?
get() throws NoSuchElementException with the message "No value present", which names neither the email nor the cause. Replace it with orElseThrow and an exception that says what was missing, for example one carrying the message "No customer with email ...". The caller and the log then show the real problem.
</details>2. A developer writes customers.findByEmail(email).orElse(createGuestCustomer(email)), where createGuestCustomer saves a new row. Ana signs in with her existing email. What goes wrong?
A guest row is created for Ana even though she already exists. Java evaluates the argument of orElse before the call, whether or not the Optional holds a value. With orElseGet(() -> createGuestCustomer(email)), the supplier runs only when no customer was found.
3. A teammate proposes Optional<List<Booking>> findBookings(long customerId). What would you suggest instead, and why?
Return List<Booking> and use an empty list when there are no bookings. An empty list already says "nothing found", and callers can loop over it with no extra check. Wrapping it in Optional creates two ways to say the same thing, an empty Optional and an empty list, so every caller must handle both.
4. GET /bookings/{id} must show a booking only to its own customer. Using Optional methods, how can one chain find the booking and apply that rule?
<details><summary>Show answer</summary>Chain findById, then filter on the customer's email, then orElseThrow. For example: findById(id).filter(b -> b.getCustomer().getEmail().equals(currentEmail)).orElseThrow(...). A booking of another customer becomes empty after the filter, so the request fails the same way as for a missing booking. That also hides from Ben whether booking 1 exists at all.
Reading Progress
0%
On This Page
What Optional Is For
Repository Results
The Methods to Use
orElse Against orElseGet
The Methods and Uses to Avoid
Key Takeaways
Practice Questions