Value Types in Java — a quest for immutability

I saw this discussion on Martin Fowler’s recent post on ValueObjects pop up in HN today; and thought that this is the right time to talk about how we (ClearTax) create value objects (value types, immutable objects, etc) in Java.

Basically, to my understanding, a value object has the following characteristics:

  • It’s immutable: You cannot change it once constructed.
  • It implements some way to check for structural equality. Basically, when talking about Java, it implements the equals method.
    Two value objects with the same data are indistinguishable — the focus is on the data the object holds, not on the object identity!
  • It’s frozen against inheritance, etc. You cannot subclass it, you cannot extend it.

These restrictions give you a lot of benefits. Immutable objects are safer: you can pass them to a method and be sure that there would not be any side effects (to that particular object at least!). This makes reasoning about the code simpler, and you don’t have to make defensive copies.

There is a reason that a lot of the primitives in the Java class library (Strings, BigIntegers, etc) are immutable.

Project Lombok

It’s possible to do this in Java, but not very easy. Project Lombok (don’t let the home page scare you away!) is a set of Java annotations that make writing code much simpler. Basically, macros for Java. It makes it dead-simple to work with value objects.

With Lombok, just use the Value annotation on your class:

public class Contact {
private String name;
private String email;
private String phoneNumber;

The annotation will automatically:

  • Mark all fields as final
  • Create a default constructor with all arguments
  • Create getters (but no setters)
  • Automatically implement equals, hashCode, toString methods

This is great! But for any larger object, we still face some issues:

  • If you have many fields in your class (say 7–8), using a constructor with that many parameters is cumbersome and error prone.
  • It’s frequently necessary to modify the object and create a new version with some fields change. This is annoying to do by hand.

The solution to both these problems is to use a Builder annotation.

@Builder(toBuilder = true)
public class Contact {
private String name;
private String email;
private String phoneNumber;
// Usage: Incrementally create a new object using the builder
// The builder provides a fluent api to initialise fields
final Contact me = Contact.builder()
// Usage: Mutate an existing object
// Change some fields and get a new version
// Original object is *not* modified
final Contact updated = me.toBuilder()
.name("Ankit Solanki")

The toBuilder pattern is a god-send: it makes it possible to actually do transformations on your immutable objects in a simple, readable manner. This is actually similar to how you can copy and update record types in F#, BTW.