skip to content

When designing a Null Object implementation of an interface, how do you decide what each method should return, and what makes some methods impossible to implement neutrally?

level: seniorimportance: should knowfreq 35%

answer

  1. Command → empty body
  2. Query → identity element (0, 1, "", empty, false)
  3. Object return → that type's null object
  4. No neutral: identity, effects, caller-dependent math
  5. Throwing null object = Liskov violation

basics

~20 s

Return the neutral value for how the caller uses the result: nothing for commands, 0 for sums, empty string or empty collection, false for permission checks, and another null object for object-returning methods. Methods asking about identity or existence have no neutral answer.

solid answer

~50 s

Work method by method. Commands get empty bodies. Value queries get the identity element of the operation the caller performs — 0 for addition, 1 for multiplication, empty collection for iteration, false for capability checks. Object-returning queries must return that type's own null object, otherwise a chain reintroduces the null one hop later. Iterators/streams return empty. The pattern breaks down for methods with no neutral answer: identity (`id()`, `email()`), methods whose contract promises an effect (`charge()`, `save()`), and methods whose neutral value depends on the caller's operation — 0 is neutral for a sum and catastrophic for a product or a divisor. When several methods have no honest neutral answer, the abstraction is doing too much (Interface Segregation) or absence is not really neutral; split the interface, or model the case explicitly with Fowler's Special Case (`UnknownCustomer`) or Optional. A null object that throws from some methods is a Liskov violation dressed as a pattern.

code

pseudocode · 12 lines
pseudocode
class NullCustomer implements Customer {
    discount()  { return 0 }              // identity for subtraction
    multiplier(){ return 1 }              // identity for multiplication
    canEdit()   { return false }          // deny-by-default
    orders()    { return emptyList() }    // empty is its own null object
    address()   { return Address.NONE }   // recurse, never return null
    notify(msg) { /* no-op command */ }

    // id() intentionally ABSENT from this interface:
    // there is no harmless identity, so identity lives on a
    // narrower interface that NullCustomer does not implement.
}

go deeper

for a junior

Say commands do nothing and queries return harmless defaults like 0, empty string, or an empty list.

for a middle

Explain the identity-element idea per operation, insist on empty collections and recursive null objects instead of returning null, and note the shared immutable instance.

for a senior

Cover where neutrality is impossible — identity, contractual effects, caller-dependent math — and the remedies: interface segregation, Special Case, Optional; call out throwing as a Liskov violation.

for a principal

Treat it as an interface-design signal: the difficulty of writing a null implementation measures how cohesive the abstraction is, and add operational concerns — naming, observability of no-op paths, preferring standard library no-ops over bespoke classes.

### The design question Once you decide to add a null implementation of an interface, every method on that interface must be answered. "Do nothing" is unambiguous only for methods that return nothing. Everything else requires a judgment: **what value makes the caller's subsequent computation behave as if the collaborator were not there?** ### Method-by-method rules **1. Commands (void / unit / no meaningful return).** Empty body. `void log(msg) {}`, `void publish(event) {}`. This is the easy case and the one the pattern was named for. **2. Value queries — return the identity element.** In algebra, the *identity element* of an operation is the value that leaves the other operand unchanged: 0 for addition, 1 for multiplication, `""` for string concatenation, `true` for logical AND, `false` for OR. Pick the identity of *how the caller combines the result*: - `discount()` feeding `total - discount` → `0` - `multiplier()` feeding `price * multiplier` → `1` - `canEdit()` feeding an access decision → `false` (deny by default — the secure neutral) - `name()` feeding concatenation or display → `""` (or a stable label such as `"(none)"` if the value is displayed, never `null`) **3. Collection / iterator / stream queries.** Return an *empty* collection or stream, never null. Empty is already the perfect null object for iteration: `for (x in empty) {}` runs zero times with no branch. **4. Object-returning queries — return another null object.** If `Customer.address()` returns `Address`, then `NullCustomer.address()` must return `Address.NONE`, not null. Otherwise `customer.address().city()` crashes and you have merely relocated the problem. This recursion is what makes the pattern hold across a chain, and it is also its main maintenance cost: introducing a null object for one type often pulls in null objects for its neighbors. **5. Self-returning / fluent methods.** Return `this` so builders and decorators keep chaining inertly. **6. Boolean "is this the null one?"** Some designs add `isNull()`. Treat it as infrastructure only (logging, assertions, framework wiring). If ordinary client code branches on it, the pattern has failed and you have the duplication of null checks plus an extra class. ### Where neutrality is impossible Three categories resist a neutral answer: - **Identity questions.** `id()`, `email()`, `accountNumber()`. There is no harmless customer id; any value you invent will eventually be persisted, compared, or emailed. Returning a fake identity is how null objects leak corrupt data into databases. - **Contractual effects.** `charge(amount)`, `save()`, `acquireLock()`, `authenticate(token)`. The method's contract *promises* something happened. A no-op that returns "ok" is a lie; the caller proceeds on a false premise. This is the single most dangerous misuse of the pattern. - **Caller-dependent neutrality.** The null object cannot see how its result is used. `rate()` returning `0` is fine inside `sum + rate` and disastrous inside `total / rate` or `price * rate`. If different callers combine the value differently, no single constant is neutral for all of them. ### What to do when neutrality fails - **Segregate the interface.** If the inert half is clean and the other half is impossible, the interface bundles two roles. Split it (Interface Segregation Principle) so the null object implements only the role where doing nothing makes sense — typically the *notification/observation* role, not the *transaction* role. - **Use Special Case instead.** Fowler's Special Case pattern is Null Object generalized: named subclasses like `UnknownCustomer`, `SuspendedCustomer`, `MissingProduct` that carry *some* behavior and a meaningful display value, rather than pretending to be nothing. This handles "absence with a reason". - **Make absence explicit.** Optional/Maybe or an explicit `Result` at the boundary, so the caller decides. - **Never throw from a null object.** `NullCustomer.id() { throw ... }` breaks the Liskov Substitution Principle — the substitute is no longer usable everywhere the base type is, so callers must re-learn which methods are safe, which is a null check with worse ergonomics. ### Practical construction notes - Make it **immutable** and expose a **single shared instance** (`Customer.NONE`, `Logger.NO_OP`) — stateless behavior means no allocation and no thread-safety concerns. - Give it a **descriptive name** (`NoOpMetrics`, `GuestUser`, `SilentLogger`) rather than `NullX`; the name is where the meaning of the absence is documented. - Keep `equals`/`hashCode` sane: identity comparison to the shared constant is the usual test, and two null objects of the same type should be equal. - Consider **observability**: a production no-op that also increments a "null-object used" counter (or logs once at startup) keeps silent absence from becoming an invisible outage. - Some ecosystems ship these already: no-op logger implementations, empty collections/iterators, empty streams, and "null" audio/graphics devices — prefer the standard one over a hand-rolled class.

  • Your null object needs to implement a method whose only honest answer would be to throw. What does that tell you about the design?
    That the interface mixes a role where doing nothing is valid with a role where it is not. Split the interface so the null implementation covers only the inert role, or drop the null object and represent the case explicitly with Optional or a Special Case subclass — throwing would break substitutability.
  • Why is returning 0 not always a safe neutral value?
    Neutrality is relative to the caller's operation. 0 is the identity for addition but annihilates a product and blows up a division. Since the null object cannot see how its result will be combined, a value used in several different operations has no single neutral choice.

Filling in a form with blanks is fine for optional fields, harmless for a comments box, and forgery the moment you invent an account number to satisfy a required field.

context