Low Level Design
Null Object Pattern — Eliminate Null Checks in Java
How the Null Object pattern replaces null references with do-nothing objects that implement the expected interface, removing defensive null checks throughout your codebase.
What is the Null Object Pattern?#
The Null Object is a behavioral design pattern that provides a default object with neutral (do-nothing) behavior to avoid null reference checks.
Instead of returning null, return an object that implements the expected interface but performs no operations (or returns safe defaults).
You reach for Null Object when:
- You want to eliminate null checks scattered throughout your code
- You need a default behavior for missing or optional objects
- You want to prevent NullPointerException at runtime
- You have optional dependencies that might not be configured
Real-World Analogy#
Think of a TV remote with dead batteries. Instead of having no remote (null), you have a dummy remote that looks and feels the same — you can still press buttons without the remote breaking, but nothing happens on the TV.
This prevents you from checking "do I have a working remote?" before every button press. The interface is identical; the behavior is neutral.
Class Diagram#
Violation Code — The Problem#
class User {
private String name;
private String email;
User(String name, String email) {
this.name = name;
this.email = email;
}
String getName() { return name; }
String getEmail() { return email; }
}
class UserService {
User findUserById(int id) {
if (id == 1) return new User("Alice", "alice@example.com");
return null; // ❌ null as sentinel
}
}
// Client — null checks pollute business logic
public class Main {
public static void main(String[] args) {
UserService service = new UserService();
User user = service.findUserById(999);
if (user != null) { // ❌ check 1
System.out.println(user.getName());
}
if (user != null) { // ❌ check 2
System.out.println(user.getEmail());
}
if (user != null) { // ❌ check 3
processUser(user);
}
}
static void processUser(User user) {
if (user != null) { // ❌ check 4 — can't trust callers
System.out.println("Processing: " + user.getName());
}
}
}
Issues:
- Repetitive null checks — every method call needs guarding
- NullPointerException risk — miss one check and the app crashes
- Polluted business logic — null checks are mixed with real logic
- Maintenance burden — adding new methods means updating all null checks
- Inconsistent behavior — different parts of the code may handle null differently
Enhanced Code — Null Object Pattern#
// Common interface
public interface User {
String getName();
String getEmail();
void displayInfo();
boolean isNull();
}
// Real object — actual user with data
public class RealUser implements User {
private final String name;
private final String email;
public RealUser(String name, String email) {
this.name = name;
this.email = email;
}
@Override public String getName() { return name; }
@Override public String getEmail() { return email; }
@Override public boolean isNull() { return false; }
@Override
public void displayInfo() {
System.out.println("User: " + name + " <" + email + ">");
}
}
// Null object — safe do-nothing implementation
public class NullUser implements User {
@Override public String getName() { return "Guest"; }
@Override public String getEmail() { return "N/A"; }
@Override public boolean isNull() { return true; }
@Override
public void displayInfo() {
System.out.println("No user found — using guest defaults.");
}
}
// Service — never returns null
public class UserService {
private static final User NULL_USER = new NullUser();
public User findUserById(int id) {
if (id == 1) return new RealUser("Alice", "alice@example.com");
return NULL_USER; // ✅ always returns a valid object
}
}
// Client — no null checks needed
public class Main {
public static void main(String[] args) {
UserService service = new UserService();
User user = service.findUserById(999);
user.displayInfo(); // ✅ safe — NullUser prints guest message
System.out.println(user.getName()); // ✅ returns "Guest"
processUser(user); // ✅ no guard needed
}
static void processUser(User user) {
System.out.println("Processing: " + user.getName()); // always safe ✅
}
}
Common LLD Problems Using Null Object Pattern#
1. Logger System#
- Null Object: NullLogger
- Context: When no logger is configured, NullLogger silently ignores all log calls instead of throwing NullPointerException.
2. User Profile / Authentication#
- Null Object: GuestUser, NullUser
- Context: When no user is logged in, use GuestUser that returns safe defaults for name, role, and permissions.
3. Payment System#
- Null Object: NoPaymentMethod
- Context: When no payment method is configured, avoid runtime exceptions with a dummy that returns empty results.
4. Notification System#
- Null Object: NoOpNotifier
- Context: When notifications are disabled, use a null notifier to avoid conditional logic everywhere.
5. Discount Calculation#
- Null Object: NoDiscount
- Context: When no discount applies, NoDiscount.apply(price) returns the original price — no null check needed.
6. Analytics / Tracking#
- Null Object: NullTracker
- Context: In test environments or when tracking is opted out, use a no-op tracker.
Null Object vs Optional#
Both address null-safety, but differ in approach:
| Null Object | Optional (Java) | |
|---|---|---|
| Type | Full polymorphic object | Container/wrapper |
| Usage | Works anywhere the interface is used | Explicit .get() / .orElse() calls |
| Behavior | Has its own (neutral) behavior | Just signals presence/absence |
| Polymorphism | ✅ Full | ❌ None |
| Best for | Behavioral defaults | Simple value absence |
| References | Links |
|---|---|
| Article Reference | Refactoring Guru — Null Object |