EXPEDIA GROUP TECHNOLOGY — ENGINEERING
Avoid Optional or Nullable Attributes with This Simple Trick
Simply take them out in this way
TLDR, summary
Optional, nullable attributes needlessly introduce state to your objects and needlessly makes your code more bug-prone and more complex. By default, optional, nullable attributes can and should be avoided. A very simple and idiomatic way to avoid them is to just take them out.
Longer version
How many times have you come across classes like this?
Note, overuse of String and Int for everything is a problem all of its own, “primitive obsession”, but that’s a different topic altogether — the topic of this post is strictly about how to compose classes)
class User {
String userId;
String username;
String bestFriend;
String favoriteBand;
}
where userId
and username are the essential attributes that constitute the core thing the class represents, and bestFriend
and favoriteBand
are optional, nullable attributes, and don’t really have anything to do with what constitutes a user.
You’ve probably come across this more times than you can count, even in fresh code, right? Probably most classes in our codebases contain a mix of non-nullable and nullable attributes. (or at least, a very significant share do)
Why it’s bad
Because any given object can now be in the state of
- having all attributes
- all but
bestFriend
- all but
favoriteBand
- just
userId
andusername
.
That’s 4 (!) states! Vs just the 1 possible state if it always had all attributes.
Note how only 2 optional attributes added 4 states — the range of states scales exponentially the more optional attributes there are.
Aside from the mere conceptual mental complexity, now, very real errors will result from doing things like
listOfFriends.add(someUser.getBestFriend()); //java.lang.UnsupportedOperationException
So now, every part of the code that deals with these objects will have to take the state into account and deal with the possibly absent values, eg:
if (someUser.getBestFriend() != null) {
listOfFriends.add(someUser.getBestFriend());
} else { //do nothing }
Worse, the compiler won’t warn about the problem, it will just result in runtime errors in production if not dealt with.
Also, conceptually, when you for example want the best friend of someone, you want the best friend object, not the originating user (let alone all details about that user) who considers that person their best friend!
The solution
Optional, nullable attributes can very simply and idiomatically be completely eliminated by just moving them out:
class User {
String userId;
String username;
}
and turned into their own thing:
class BestFriend {
String originUserId;
String bestFriendUserId;
}
class BestFriendService {
Optional<User> findBestFriendOf(String userId);
}class FavoriteBand {
String userId;
String bandId; //or String bandName or whatever
}
class FavoriteBandService {
Optional<User> findBestFriendOf(String userId);
}
Note that this isn’t some “clever” solution, and it doesn’t rely on annotations, Optional, Kotlin nullable attributes etc. — we’re literally just moving the concept of a best friend out of the user class to be their own thing — which it is! It doesn’t belong on the user object!
Common arguments
These are just some of the most common ones I see, but I’m sure more could be thought of.
Leaving the attributes on the User and using Kotlin Types? / Optional / @NotNull and @Nullable solves it
No, it doesn’t. All they do is make the compiler warn and prevent things like:
listOfFriends.add(someUser.getBestFriend()); //java.lang.UnsupportedOperationException
which is great! But the fundamental problem of state and complexity (both conceptual and in concrete code) is still there (that is, when working with the object we still have to do if/map or whatever), and the class remains poorly designed, with attributes that don’t really have anything to do with what constitutes a user.
Code review will catch any resulting bugs
Maybe! By all means, you could carefully code review to make sure there are no bugs resulting. But… why not just code in a way that completely avoids the issue, greatly simplifying the code review, so you won’t have to (hopefully) catch them in a code review?
What about making the attributes non-optional with default values?
Sometimes, this is an option (pun intended). But who are you going to set as bestFriend
, and what band are you going to set as favoriteBand
? It’s not always possible to set a default — many times, defaults are just set to shoddy non-values like “NA” or “unknown” or whatever just to please the compiler, when, really, that’s a strong smell that something isn’t right and there’s an underlying issue that should be fixed instead — the classes should be redesigned.
Sometimes attributes really are optional, nullable / sometimes optional, nullable attributes are useful, practical etc
Sure! Despite what it may seem, I’m actually not making some fundamentalist argument that absolutely no class must ever contain optional, nullable attributes. I’m simply saying, rather than by default casually and liberally adding optional, nullable attributes to classes, instead, by default, try to keep classes free of optional, nullable attributes as much as possible, and only introduce them when truly appropriate.
What if I always need to load users including their best friend and favorite band
In that case, by all means, maybe splitting things up like this is pointless. But even then, it could still be beneficial to keep things separate, because it is good class design. Consider the fictional example below. It’s based on some similar code that was actually being used in production.
Note, the attributes can be null
public class User {
private int firstAttribute;
private String secondAttribute;
private Byte thirdAttribute;
private Byte fourthAttribute = 0;
private String fifthAttribute = 0;
private int sixthAttribute;
// etcetera
}
Granted, this may seem like a contrived and trivial example, but it serves to demonstrate how things can get out of hand if good class design is not kept in mind from the outset.
Other considerations
Benefits the database too
Since class design often maps closely to underlying persistence, the proposed solution will have the same added benefit to your persistence layer, whether using relational database schema or objects stored as JSON. For example instead of your tables looking like this:
mysql> select * from user;
| user_id | username | best_friend_user_id | favorite_band |
| xxx | foo | null | null |
| yyy | bar | zzz | null |
| zzz | smurf | yyy | The Smurfs |
They will look like this:
mysql> select * from user;
| user_id | username |
| xxx | foo |
| yyy | bar |
| zzz | smurf |
and
mysql> select * from best_friends;
| first_user | second_user |
| xxx| xxx |
...mysql> select * from favorite_bands;
| user_id | favorite_band |
| xxx | The Smurfs |
...
Benefits infrastructure and performance too
Often, you’ll find that optional, nullable attributes are fundamentally different in ways that relate to their:
- nature — they often turn out to constitute relationships between things rather than things themselves, which is true for both
bestFriend
andfavoriteBand
. - “load profile”— while for example a user object
userId
andusername
only need to be loaded very rarely, and changed even more rarely,bestFriend
andfavoriteBand
might change more often and have different caching and time to live. We don’t want to needlessly add traffic to the user table, reload and store the whole user into caches and so on when really we just want to updatebestFriend
andfavoriteBand
!
So splitting things up helps with that too, especially when operating at scale as we do at Expedia Group™️.