You Should Be Using Immutables

By Justin Standard — Senior Software Engineer

The hardest thing about being a Java developer in 2017 is also the easiest thing about being a Java developer in 2017: there are so many great frameworks, libraries, and tools to choose from that, like the proverbial kid in the candy store, you might just get so excited that you enter a state of analysis paralysis.

And once you manage to stop hyperventilating from the overwhelming wealth of mavenized-github-repo-riches that lies so near within your grasp, far too often, a familiar scenario plays out. A team member takes some time to spike a new technology. Meetings occur. Buzzwords are spoken. Pros and cons assessed. Joe JuniorDev mentions the comparable tool he heard about at the local meetup and Sally SeniorDev waxes poetic about that other thing that was presented last year at JavaOne. Comparisons are contrasted until choice overload leads to decision fatigue and in the end, those shiny new toys are left on the shelf in favor of the tired old patterns that left you wanting in the first place.

Many have stumbled down this unfortunate path, but fear not, gentle reader. For you have arrived at my doorstep, and I’m here to tell you exactly what you should do.

You should be using Immutables

Immutables is a Java library that lets you create simple and beautiful value objects. Objects unlike the POJOs that power your current data model. Objects that will make you smile instead of cry. With Immutables, simply declare an interface (or an abstract class) which describes your data, mark it with the @Value.Immutable annotation, and Immutables will generate your entire POJO with all the trimmings. Trimmings? Oh yes, you get them all (including implementations of hashCode(),equals(), and toString(), null validations, and a powerful Builder). And because your shiny new immutable data objects are generated at compile time, there are no new runtime dependencies introduced into your project — no new jars to ship with your finished code.

So forge ahead, dear reader, and I will explain the advantages …

But wait! Are you just trying to get me to do FP?

Certainly not! I would never!

Although functional programming languages like Scala, Clojure, and Haskell are on the rise, (we here at Grindr are really into Elixir), and while these languages have a strong preference, if not an outright requirement, for immutability, have no fear. Using Immutables for java doesn’t mean drinking the FP Kool Aid. Instead, you can keep doing what you’re doing right now, but reap the benefits of safer, more reliable, more fluent and more testable POJOs. It’s going to free you up from writing all those obnoxious getters and setters that are littering your traditional data object and free your mind to focus on that one really interesting problem you’re trying to solve.

Advantage 1: Clean POJOs

Imagine a class that models a Subscription. In a typical java POJO it might look like this:

public class Subscription {
    private Long subscriptionId;
private Long userId;
private Timestamp createdAt;
private Timestamp expiresAt;
private Product product;

public Subscription() {
}
    public Long getSubscriptionId() {
return subscriptionId;
}
    public void setSubscriptionId(Long subscriptionId) {
this.subscriptionId = subscriptionId;
}
    public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}
    // about 40 bajillion more lines of boring code
// ugh... when will it end???
// ...
    public Product getProduct() {
return product;
}

public void setProduct(Product product) {
this.product = product;
}
    // TODO implement .equals / .hashCode ...
// ... you know, when we get around to it...
}

Half the time, after writing (or hopefully, generating with your IDE) all that boilerplate code, you don’t even have the time or energy to implement a proper .equals() or .hashCode() — especially if you aren’t going to use them right away. Your toString() is, of course, just going to be the default implementation from Object. And if all this isn’t verbose enough, whenever you want to actually build one of these objects you go through a whole song and dance to do it:

Subscription mySubscription = new Subscription();
mySubscription.setUserId(42L);
mySubscription.setCreatedAt(creationTime);
mySubscription.setExpiresAt(expireTime);
mySubscription.setProduct(myProduct);
// soooo ... sad....

The more properties you use in your classic POJO, the more of this you’ll have to do every time you create an instance. (Or use a constructor with *way* too many parameters). And since each and every setter must be called individually, you can never be quite sure that your object is fully initialized. (Observant readers may notice that I failed to set subscriptionId above. Oops! Mistakes happen.) That means you will have to check for null values every time you access one of those properties. You can’t just make an inline call like

Double price = mySubcription().getProduct().getPrice();

for instance. Instead you need to.

Double price;
if (mySubcription().getProduct() != null) {
price = mySubscription().getProduct().getPrice();
}
// and then you have to figure out what to do if Product is null
// which isn't even a valid case!

Immutables to the rescue! With Immutables, you only need to define an abstract class or interface defining your data object. (I prefer abstract classes, but that’s just me.)

@Value.Immutable
public abstract class Subscription {
public abstract Long subscriptionId();
public abstract Long userId();
public abstract Timestamp createdAt();
public abstract Timestamp expiresAt();
public abstract Product product();
}

That’s it. A very nice tight description of your data object without a bunch of boilerplate crap. When you compile your project, everything you need will be generated in a class called ImmutableSubscription, which will be a subclass of Subscription. (Pro-tip — in IntelliJ go to settings and enable annotation processing to have the compiler generate your ImmutableSubclasses even if the project isn’t in a buildable state).

And as a bonus, creating your new immutable object is simple using the provided builder:

Subscription subscription = ImmutableSubscription.builder()
.subscriptionId(182L)
.userId(42L)
.createdAt(creationTime)
.expiresAt(expireTime)
.product(myProduct).build();

And since the object is immutable you don’t have to worry about side effects when passing it into some function call. If you want a slightly different copy of your object, perhaps with an updated expiration date, you can use the builder to seamlessly get one using

ImmutableSubscription.copyOf(subscription).withExpiresAt(nextMonth);

which doesn’t modify subscription but returns a new immutable object with the expiresAt property having the passed in value.

What you end up with is less code to create a safer better and cleaner POJO.

Advantage 2: Generated POJO methods

Let’s face it. Every java developer has forgotten to write an equals() and/or a hashCode() at some point. And some of us have even been burned by it.

A big problem with the traditional POJO is that overriding the critical equals(), hashCode(), and toSting() methods are optional, since theObject class already contains a default implementation. Because of that, teams often forge ahead implementing their data objects and forget to ever override these methods, whose default implementation is rarely appropriate. In particular, equals() and hashCode() are required to be consistent — if they fall out of sync then a lot of weird bugs can occur, but nothing enforces that they remain consistent.

With Immutables you get these methods for free in your generated implementation so you can happily mySubscription.equals(otherSubscription) until the cows come home without worrying about rolling your own or making an error.

I know what some of you are thinking: “Generated equals() and hashCode()? My IntelliClipse IDE can do that!”

Which is true, but it comes with a major disadvantage. You have to remember to regenerate those implementations any time the data object changes. If you don’t, you suddenly introduce all sorts of bugs when equals() and hashCode() no longer behave as expected, or possibly behave inconsistently with one another. But Immutables protects you from this. Changes to your data object will automatically be reflected in the implementation generated at compile time, and equals and hashCode will never fall out of sync.

Advantage 3: nullChecks Not Required

One frustrating thing about passing around POJOs is the amount of null checks you have to make. Because when your method signature has you passing objects around there is always a chance that the object passed in could be null, or have null elements.

Consider the following case with our Subscription example. Imagine that you have Products that grant one or more roles to a user and you want to check if the user’s subscription grants them a special foo role.

public Boolean isFoo(Subscription sub) {
if (sub != null) {
if (sub.getProduct() != null) {
if (sub.getProduct().getRoles() != null) {
return sub.getProduct()
.getRoles()
.contains(Role.FOO);
}
}
}
return false;
}

Not only did we have to check if the Subscription POJO passed to us was null, we had to check every sub-element, and every sub-element of that which might possibly be null if the passed in object isn’t valid.

But if you use Immutables to generate your Subscription (and Product) you get something much cleaner:

public Boolean isFoo(Subscription sub) {
if (sub != null)
return sub.product().roles().contains(Role.FOO);
}
return false;
}

Although you still have to check if the passed in object is null, no other checks are required. Why? Because if you tried to use the generated builder and failed to provide a Product you’d see something like this (at run time):

// leaving off the product...
Subscription subscription = ImmutableSubscription.builder()
.subscriptionId(182L)
.userId(42L)
.createdAt(creationTime)
.expiresAt(expireTime).build();
>>> java.lang.IllegalStateException: Cannot build Subscription, some of required attributes are not set [product]
at com.foo.model.ImmutableSubscription$Builder.build(ImmutableSubscription.java:330)
at com.SomeService.someServiceCall(SomeService.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at ...

You don’t need to check any nulls after the initial object is built because all values are guaranteed to be non null once the build()call completes, making your downstream models much easier to deal with. You do still have to check parameters to see if the passed in reference itself is null, and you will still need to ensure there aren’t any nulls when you first build your object (to avoid that IllegalStateException), but you need only do so once, not all throughout your code. And if there are any nulls, perhaps from bad user input, you can handle them properly, in just one place.

(One important note — Immutables support the @Nullable annotation on value object properties, which can be very useful at times, and if you make use of it you will still have to check for nulls as usual.)

Advantage 4: Testing is a Breeze

“To me, legacy code is simply code without tests.” — Michael C. Feathers, Working Effectively With Legacy Code

This is one idea that has always stuck with me. And you don’t want to be writing more legacy code, do you? No! So you’re writing tests, aren’t you? Yes!

Good! And Java Immutables can reduce the pain of validating your code.

Back to our Subscription example, say you were trying to test a normal POJO that grants a subscription an extra hour as a grace period. Not only do you need to verify that the Subscription gets an extra hour added to the expiration date, but you should also check that no other properties were changed, and the test is going to look roughly like this:

@Test
public void testAddingAnHourToSubscription() {
SubscriptionService subscriptionService =
new SubscriptionService();
Long startTime = Instant.now().toEpochMilli();
Long expectedExpireTime = startTime + (1000 * 60 * 60);

Subscription testSub = new Subscription();
testSub.setUserId(42L);
testSub.setSubscriptionId(123L);
testSub.setCreatedAt(new Timestamp(startTime));
testSub.setExpiresAt(new Timestamp(startTime));
testSub.setProduct(new Product());
    Subscription actual =
subscriptionService.addGracePeriod(testSub);
    assertEquals(42L, actual.getUserId());
assertEquals(123L, actual.getSubscriptionId());
assertEquals(new Timestamp(startTime), actual.getCreatedAt());
assertEquals(new Timestamp(expectedExpireTime),
actual.getExpiresAt());
assertEquals(new Product(), actual.getProduct());
// hopefully we implemented .equals on Product....
}     

It’s a lot of work just to verify that one transformation was made correctly. Now lets look at this same test if we generated Subscription using Immutables:

@Test
public void testAddingAnHourToSubscription() {
SubscriptionService subscriptionService =
new SubscriptionService();
Long startTime = Instant.now().toEpochMilli();
Long expectedExpireTime = expectedStart + (1000 * 60 * 60);

Subscription testSub = ImmutableSubscription.builder()
.userId(42L)
.subscriptionId(123L);
.createdAt(new Timestamp(startTime));
.expiresAt(new Timestamp(startTime));
.product(new Product()).build();
    Subscription expectedSub = 
ImmutableSubscription.copyOf(testSub)
.withExpiresAt(new Timestamp(expectedExpireTime));
    assertEquals(expectedSub,
subscriptionService.addGracePeriod(testSub));

// w00t! immutables implemented .equals() for us!
}

As you can see, the ability to rely on .equals() and the ability to quickly get a new copy of some test object with only slight changes makes testing much clearer and easier, significantly reducing the amount of code you have to write.

What about Collections?

Lists and Sets and Maps, oh my! These are core pieces of a developers coding toolkit, and you can bet your butt they have great support in Immutables.

In general, you can use the major collection types like Map, List, and Set just as you would expect in your Immutable classes, which will be backed by Guava immutable collections if Guava is available on the classpath. Otherwise (as with Array attributes) they are safely copied, cloned, and/or wrapped using the standard JDK. A large number of other Collection types have first class support, but there are a few gotchas to watch out for (including java.util.Collection and java.lang.Iterable) that you ought to be aware of, even though these types (like all types) can still be supported. Take a minute and check out the docs for more information on the cool collection support available as well as the edge cases to watch out for.

What About Alternatives

The idea of building a better POJO is so popular that a number of options for Value type objects have sprung up. Dustin Marx compared a few of them (Immutables, AutoValue, and Lombok) in a blog post last year, and a short time later Stephen Colebourne covered the same issue, throwing in a fourth option, Joda-Beans.

The truth of the matter is that I haven’t tried all the options. Nope. Rather than suffer choice overload, I’ve gone all-in on Immutables and I’m happy to recommend that you do the same. But if you’re looking for alternatives, these blog posts might be a good place to start.

You Should Do It

This is naturally just an introduction, but the best way to learn about the power of the Immutables library is to just start using it. Avoid that nasty analysis paralysis and take my word for it. Try Immutables. Then the next time you start sniffing around for that familiar code smell, enjoy the immutable draught of fresh air that takes its place.

Thanks for reading! As always, your constructive ideas, comments, and criticism are greatly welcome. Peace!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.