Types, Nullability, Optional
Null is not a value, it is a state
In production systems, most unexpected crashes are not caused by complex algorithms. They are caused by implicit null assumptions. A NullPointerException is rarely the root problem; it is usually the symptom of an undefined contract.
Production mindset: make nullability explicit
Every method should answer a simple question: can this return null? If yes, why? If no, enforce it. Ambiguity is the real bug.
Bad example: ambiguous contract
User findById(String id); // Can this return null? Or throw?
This forces every caller to guess. Some will check for null. Some will not. Eventually, production fails.
Better: explicit contract
// Option 1: throw if not found User getRequiredById(String id); // Option 2: express absence explicitly Optional<User> findById(String id);
Primitive vs Wrapper: performance and semantics
Primitive types (int, long, boolean) cannot be null. Wrapper types (Integer, Long, Boolean) can. In production systems:
- Use primitives for required numeric fields.
- Use wrappers only when null is a meaningful state.
- Be aware of boxing overhead in hot paths.
Hidden footgun: unboxing null
Integer count = null; int x = count; // NullPointerException
This often appears in DTO mapping or JSON parsing. Always validate before unboxing.
Optional: when to use it (and when not to)
Optional is not a replacement for every nullable reference. It is a tool to make absence explicit in API contracts.
Good use cases
- Return type of repository lookup.
- Derived computation that may not produce a result.
- Boundary APIs where absence is expected and normal.
Bad use cases
- Fields inside entities or DTOs.
- Method parameters.
- Serialization models.
Anti-pattern: Optional field
class User {
Optional<String> middleName; // Avoid
}
This complicates serialization, mapping, and increases memory overhead. Prefer nullable field + validation at boundary.
Correct Optional usage
Optional<User> userOpt = repo.findById(id);
userOpt.ifPresent(user -> {
// do something
});
// Or
User user = userOpt.orElseThrow(() -> new NotFoundException(id));
Optional anti-pattern: get() without check
userOpt.get(); // Throws NoSuchElementException if empty
This defeats the purpose. Prefer orElseThrow with explicit exception.
Fail fast at boundaries
Validate nullability at boundaries (controller, CLI, message consumer). Once inside the domain layer, assume invariants are satisfied.
Example: defensive constructor
import java.util.Objects;
public final class Order {
private final String id;
private final long amountCents;
public Order(String id, long amountCents) {
this.id = Objects.requireNonNull(id, "id must not be null");
if (amountCents <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
this.amountCents = amountCents;
}
}
Production failure scenario
A service assumes request.getUserId() is never null. In rare malformed requests, it is null. A NullPointerException propagates to the global exception handler, returns 500 instead of 400, triggers alerts, and operators investigate a non-incident.
Correct pattern
- Validate request DTO early.
- Convert to domain object only after validation.
- Keep domain model non-nullable whenever possible.
Null handling checklist
- Make return contracts explicit (Optional or exception).
- Avoid nullable primitives via wrapper types unless necessary.
- Never store Optional in fields.
- Validate at system boundaries.
- Use Objects.requireNonNull for invariants.
- Fail fast instead of allowing null to travel deeper.
Final principle
Nullability is a design decision. If you do not decide explicitly, the JVM will decide at runtime.