Sitemap

Using Optional in Java: Best Practices, Anti-Patterns, and a Practical Guide using the Customer Class

4 min readJun 19, 2026

--

In the backend world, especially if you are developing large-scale enterprise Java projects, you know all too well the infamous error we hate seeing in the logs: NullPointerException (aka NPE). To deal with this situation, known in computer science as the "billion-dollar mistake," a wonderful tool entered our lives with Java 8.

But how accurately do we position this tool in our projects? Does it really clean up our code, or does it make it more complicated? Let’s dive deep into this topic through the Customer class, which is the heart of an e-commerce or CRM system.

What is Optional?

In its simplest terms, Optional is a wrapper class that can either contain a value or be completely empty (empty instead of null). Its main purpose is to explicitly indicate to the caller (client) that a method might not return a value, forcing them to handle this scenario.

By doing this, instead of traditional and easily forgotten checks like if (customer != null), we establish a safer and more functional structure.

The Anti-Pattern Example

When Optional first entered our lives, many of us used it simply as a disguised version of our old "null checking" habit.

In the example below, we are trying to fetch a customer from the database. However, instead of modernizing the code by using the isPresent() and get() duo, we are just making it longer:

// ❌ WRONG: The old-school null check disguised as Optional
Optional<Customer> customerOpt = customerRepository.findById(101L);

if (customerOpt.isPresent()) {
Customer customer = customerOpt.get();
System.out.println("Customer Name: " + customer.getName());
} else {
System.out.println("Customer not found.");
}

This usage style harms readability and completely ignores the functional programming benefits that Optional offers.

The Best Practice

We can write the ugly and long code block above in a single line and much more elegantly using functional methods:

// ✅ RIGHT: The power of functional methods
customerRepository.findById(101L)
.ifPresentOrElse(
customer -> System.out.println("Customer Name: " + customer.getName()),
() -> System.out.println("Customer not found.")
);

If we want to transform the data or assign a default value if it doesn’t exist, we must pay close attention to the difference between orElse and orElseGet:

// ❌ WRONG and Costly: createNewCustomer() runs every time, even if the customer exists.
Customer customer = customerRepository.findById(101L)
.orElse(createNewCustomer());

// ✅ RIGHT and Performant: A new customer is created ONLY if the customer doesn't exist (Lazy).
Customer customer = customerRepository.findById(101L)
.orElseGet(() -> createNewCustomer());

For costly operations (database calls, new object creation), orElseGet should always be used.

Usage in the Service Layer

The Service (Business) layer is where business rules are executed. Optional should only be used as a return type in this layer. If the customer we are looking for doesn't exist, instead of silently bypassing the system, we usually need to throw an "Exception". The orElseThrow method is tailor-made for this scenario.

@Service
public class CustomerService {

private final CustomerRepository customerRepository;

public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}

// Using Optional in the method return type to inform the client
public Optional<Customer> findCustomer(Long customerId) {
return customerRepository.findById(customerId);
}

public Customer getCustomerDetails(Long customerId) {
// Return if the customer exists, otherwise throw a custom exception
return findCustomer(customerId)
.orElseThrow(() -> new CustomerNotFoundException("Customer with ID " + customerId + " not found!"));
}

// ❌ WRONG: Optional SHOULD NOT be passed as a parameter to methods!
// It forces the caller to constantly write Optional.of() and pollutes the method signature.
public void updateCustomer(Optional<Customer> customer) { ... }
}

In Entity Fields

One of the most common mistakes developers make is defining “nullable” columns in the database as Optional in Entity classes. For example, a customer might not have a middle name or a phone number.

Get EMİR GÜLER’s stories in your inbox

Join Medium for free to get updates from this writer.

Why shouldn’t we do this? The Optional class does not implement the Serializable interface. This situation will cause you headaches when ORM tools like Hibernate or JPA are managing your Entities.

@Entity
public class Customer {
@Id
private Long id;

private String name;

// ❌ WRONG: Optional is not used in Entity fields!
// private Optional<String> phoneNumber;

// ✅ RIGHT: The field is defined with its standard type
private String phoneNumber;

// ✅ BEST: Optional is returned only in the Getter method
public Optional<String> getPhoneNumber() {
return Optional.ofNullable(phoneNumber);
}
}

In DTO Fields

The situation in the Data Transfer Object (DTO) layer is no different from Entities. The main purpose of DTOs is generally to transform data into JSON format (Serialization) and transport it over the network.

Popular JSON serialization libraries like Jackson or Gson cannot properly parse Optional fields inside classes (unless you install additional modules). Also, using Optional with collections (List, Set) is a situation to be avoided entirely.

public class CustomerDto {
private String name;

// ❌ WRONG: Leads to JSON parse errors in REST endpoints
// private Optional<String> email;

// ✅ RIGHT: Leave it as the standard type
private String email;

// ❌ WRONG: Collections can already be empty, they shouldn't be wrapped in Optional.
// private Optional<List<String>> roles;

public String getEmail() {
return email;
}
}

DTOs should be “dumb” data carriers. Architecturally, it is much more correct to do the presence/absence (null) checks in the Controller or Service layer that receives this data.

In Summary

Optional is a great tool for Java developers. But like any tool, it brings more harm than good when not used for its intended purpose. If we want to write clean and maintainable code; we should make it a standard to use this powerful tool only in method returns and alongside functional chains, not in class fields or parameters.

What is your favorite “Optional Anti-Pattern” that you encounter most often in your projects or are tired of fixing in Code Reviews? Let’s meet in the comments!

--

--