When it comes to creating objects in Java, we can use fluent approaches, especially for complex objects containing lots of fields, that will increase readability and also adaptability allowing us to evolve the code with lower impact on the existing code. And we want to because fluent code is both easier to read and easier to write.
We also know that immutable objects are easier to maintain, lead to fewer errors, and are multi-thread friendly.
In this article, I will talk about two different approaches to creating objects: Builders and Withers, typically used in the context of immutable objects, along with a new type of immutable object in Java: Records.
JavaBean pattern
The usual way of defining classes in Java follows the JavaBean pattern. This involves using a default constructor with no arguments, and accessors and mutators for properties.
This approach implies that the state of the object can be “unsafe” as we could create an instance of Person without specifying any mandatory and key values. It even allows mutating the object during its lifetime, potentially making the system less safe, especially with multithreaded approaches. Immutability brings a lot of benefits.
The path to immutability and a safe state
So, the next step in order to fix this issue would be to create a constructor with the mandatory and key properties, and not expose mutators (setters) for them.
With this approach though, we face potential issues in terms of readability and adaptability when the class grows into a more complex definition.
In case we add more mandatory properties, as we see above, we need to add more parameters to the constructor and this will impact the existing code making us modify it on every call to the constructor.
Considering mandatory and optional arguments, for immutable objects, we can run into the “telescoping constructors” problem where we need to create several constructors considering the different nullability combinations.
The Builder approach
To fix this we can use Builders, which will help with readability and also on future changes making it easier to add the new properties.
First let’s remove any mutator, leave the accessors, and make it “impossible” to create a new instance with a constructor.
Now we will add the inner class in charge of building the new instance and a new method that invokes the Builder.
And with this approach now we are able to create a new immutable instance with a validated state.
The above approach includes a lot of boilerplate code that can discourage us from using it. To make things easier we can use libraries with annotations that will generate the code for us: Immutables, Lombok, Auto, FreeBuilder, etc.
The Wither approach
Another approach to having a fluent API and immutability is the usage of “withers”, or with* methods, that create a new instance on every property change.
The idea behind it is that every mutator creates a new object instance, and we can chain those calls in order to produce complete instances.
We can consume this approach like this, making it very easy to apply small changes to an existing object by obtaining a new object. We are “cloning” the object and changing one property at a time.
Again in order to reduce boilerplate code, and be less error-prone, we can leverage existing libraries with annotation processors that will make the process smoother and cleaner.
The main drawback to the Withers approach is that we rely a lot on the garbage collector in order to remove intermediary objects, especially when we chain Withers person.withName(“John”).withAge(50).
Those objects are not used in the end and we will need to wait for the garbage collector to remove them. This can impact performance in systems with high object creation rates.
Records
Finally, the language itself, since Java 16, provides a struct definition called Records, which is focused on immutability, mainly to store data values, reduce boilerplate code, and increase readability.
With Records, we can be sure our objects are immutable as they don’t provide mutators, only accessors, and fields are final.
So in our case, our Person class could be defined as
This would end up in the same code for Person as we had at the beginning of this article, removing the setters and making all fields final.
Some creational issues are not solved out of the box with Records, like the mandatory/optional fields and the constructor, and it’s not easy to create new objects based on existing ones, but we can rely on libraries like RecordBuilder to help us with that.
Despite these issues, Records are a great solution for representing data with immutable state, while also reducing the boilerplate code in order to define the structures.
Conclusions
Immutability is a concept that will provide many benefits to our code, like predictability, easy testing, thread safety, and others that will impact our code’s intentionality, consistency, adaptability, and responsibility.
In order to achieve immutability we have different options like Builders, Withers, or the use of Record type, but ultimately, the choice between Builders and Withers depends on the specific requirements of your application and the design principles you want to follow. Builders are often preferred for complex object creation with many optional parameters, while withers can be more suitable for modifying existing immutable objects. If you are on Java 16 or above consider that the use of Records is recommended over ordinary classes as they are immutable per definition.
Remember that SonarQube for IDE, SonarQube Server, and SonarQube Cloud with their Java analyzer will help you deliver clean code with a long list of rules to consider when you code.