Implementing nullable object state

Ürgo Ringo
3 min readAug 1, 2020
Nullable lamp. Photo from private collection.

Previously I wrote about avoiding nulls using NullObject and Optional method return value. In this post I will focus on modelling optionality in an object internal state in cases where NullObject is not suitable. This means when implementing the basic building blocks for internal state of Value Objects and Entities or working with data structures instead of behaviour.

If we keep our classes small then this is a much smaller problem than having nulls passed around between objects. Still, the more of the actual domain constraints we are able to convey in our static code structure the better. This means making the optionality obvious instead of forcing the reader to look up usages of the field.

In general we have two choices:

  1. use annotations like Lombok NotNull to mark what fields cannot be null
  2. use some kind of optional value container type like Vavr Option (note that we should not use Java Optional for fields — more details)

Annotations approach

With Lombok it looks like this:

@Getter
@AllArgsConstructor
class Customer {
@NotNull
Id id
@NotNull
Name name
@Getter(AccessLevel.NONE)
Email email
... Optional<Email> getEmail() {
return Optional.ofNullable(email)
}
}

The main benefit of this solution is that it does not require any new dependencies assuming that Lombok is used anyway.

However, there are several drawbacks. Typically most of the fields should not be nullable. So it is counterintuitive that we need to mark the fields following the general rule of non nullability instead of marking the nullable as a special case.

Secondly, it is too easy to forget adding the NotNull annotation since it does not directly affect our own code. This means not having a NotNull on a field cannot be trusted as it may be an omission by accident.

Thirdly, Lombok NotNull is just an explicit NPE so does not really offer protection on the outer edges (e.g when handling incoming HTTP requests) of our application. Without tests it will just buy us early NPE at runtime and save some debugging effort.

We can alleviate some of these problems by using static code quality checkers like FindBugs or NullAway. The latter can be used with Nullable annotations. Main problem with static checkers is that unless they integrate seamlessly with our IDE or local build they are easy to ignore.

Optional value container approach

With optional value containers like Vavr Option the main drawback is having to introduce a new library. However, this approach allows to make the optionality of a value really explicit. We force ourselves to handle the null case and don’t rely on some external tool to do the verification for us.

I would not use Option for any parameters though. Providing overloaded versions of constructor and methods is more convenient for consumers. It looks like this:

@Getter
@AllArgsConstructor(AccessLevel.PRIVATE)
class Customer {
Id id
Name name
Option<Email> email
public Customer(Id id, Name name) {
this(id, name, Option.none())
}
public Customer(Id id, Name name, Email email) {
this(id, name, Option.of(email))
}
}

There is no need to manually implement getter for the optional “email” field which is another small benefit.

What is really awesome with this approach is that we can also use Option in our command objects when transforming HTTP requests. This allows us to handle the nulls before they reach deeper into our application.

vavr supports jackson serialization/deserialization with vavr-jackson lib (in Spring we just need to declare new VavrModule bean).

An example new customer command with optional email:

@Getter
@Setter
class NewCustomerRequest {
String firstName
String lastName
Option<String> email
}

This is a lot more elegant than having to remember that some fields may be null whenever working with the command object.

Summary

Both solutions to nullability require everyone in the team to follow the agreed approach. However, I think modelling optional fields using optional value containers like Option is more explicit and easier to follow than using annotations no matter how these are processed. The benefits of Option become especially obvious when implementing command objects for handling incoming requests.

--

--

Ürgo Ringo

Have been creating software for 20 years. Cofounded a software consultancy, worked as an IC and team lead at Wise. Currently working at Ibank.