This is a part of this post: Huh? to Aha! - A Refactoring Story
The Entanglement
Mutability causes a lot of problems, especially the Shared mutable state on a Shared Codebase. Let’s focus on how it hinders the goal of Component Isolation.
Mutable Objects as Input params
- This is just a simple function, which takes a list of numbers and sums them up.
static int sum(List<Integer> nums) { int result = 0; for (int num : nums) result += num; return result;}- Let’s say a developer wanted to extend its functionality to calculate absolute sum and wanted to reuse
sumlike this:
static int sumAbsolute(List<Integer> nums) { for (int i = 0; i < nums.size(); i++) { nums.set(i, Math.abs(nums.get(i))); } return sum(nums); // DRY}- Now a client uses it, and everything worked, they released it and had a GA party! 🥳
static void client() { var nums = Arrays.asList(-2, 5, -6); System.out.println(sumAbsolute(nums));}- Some day after the release, a developer wanted to insert this line in the client code, and he got this unholy result.
static void client() { var nums = Arrays.asList(-2, 5, -6); System.out.println(sumAbsolute(nums)); System.out.println(sum(nums)); // 13 👺}- After a painful debugging session, he found a bug; that has been waiting to bite him — A Latent Bug.
static int sumAbsolute(List<Integer> nums) { for (int i = 0; i < nums.size(); i++) { nums.set(i, Math.abs(nums.get(i))); // Latent Bug 🐞 } return sum(nums); // DRY}
static void client() { List<integer> nums = Arrays.asList(-2, 5, -6); System.out.println(sumAbsolute(nums)); System.out.println(sum(nums)); // 😬}Because the sumAbsolute received a reference to a mutable object, it assumed it as a license to do any mutations on it. To me the mistake lies with the client, who passes-around a mutable object reference. This mutable object acts as an invisible string, coupling the components sumAbsolute, client and sum. Thus, Mutable objects as Input params are Unholy for isolation.
Mutable Objects as Return types
Well, that’s even more dangerous. Let’s see with an example: This is a function, which takes an eggId and fetches its laying date by doing a heavy DB operation.
Date getEggLayingDate(int eggId) { // heavy operation return queryEggLayingDateFromDB(eggId);}- Assume, it has two dependent components, independent of each other. Assume both these components are present in two different modules.
// Dependent component - 1boolean isLaidInFirstHalf(int eggId) { var layingDate = getEggLayingDate(eggId); if (layingDate.getDate() < 15) { return true; } return false;}
// Dependent component - 2int calculateEggAge(int eggId, Date today) { return today.getDate() - getEggLayingDate(eggId).getDate();}- They are all in harmony, everything is working great. One day! a developer wanted to add a trivial log in
hasEggRotten. - As a date object is not used anymore in this function, he felt he could reuse this object to get month and year information and so he mutated it like this:
// Dependent component - 1boolean isLaidInFirstHalf(int eggId) { var layingDate = getEggLayingDate(eggId); if (layingDate.getDate() < 15) { // It's just logging, let's reuse the same Date obj for month and year layingDate.setDate(15); logger.info("This egg was laid before: " + layingDate); return true; } return false;}- As they expected, there were no problems reported. Another day! the developer of
getEggLayingDatefelt there is an opportunity to optimize his DB heavy component and so implemented caching like this.
static final Map<Integer, Date> eggLayingDateCacheById = new HashMap<>(); // CacheDate getEggLayingDate(int eggId) { return eggLayingDateCacheById .computeIfAbsent(eggId, this::queryEggLayingDateFromDB);}- He tested his component, and it’s working great and so they released it.
- Now, guess who gets the bite this time? The innocent
calculateAgecomponent which has no changes!
// Dependent component - 2long calculateEggAge(int eggId, Date today) { return today.getDate() - getEggLayingDate(eggId).getDate(); // What did I do? 😿}- We just witnessed, how components separated by modules can be entangled with Mutable Objects. It’s almost impossible to refactor them without breaking anything.
- On a real code base, this is even more intertwined. Most of our debugging cycles are spent to bash bugs like these. It resonates with the universal developer experience - “Fix this breaks that”.
- I call this a Quantum Entanglement! ⚛🧙🏼♀️
References Everywhere
It’s not just the Mutable objects but Java has pointers all around. References are writable by default.

Looking at the numbers of views, up-votes and bookmarks, I am sure a lot of developers have been bitten by this.
But why is Mutability Predominant in Java code?
Because historically it has been the default mode in Java, and defaults are powerful. We all heard Google pays Apple a fat cheque every year just to keep google as default search engine. People seldom change defaults.
It takes Discipline to beat the Default
- Just like how we work-out every day and how we need to follow traffic rules. Remember, traffic rules are not made for good drivers. Even if there are 99 good ones, one bad driver can create havoc on the road.
- We cannot depend on developer niceties and expect them to follow some coding-guidelines documented somewhere which no one refers.
- It’s important we imbibe those restrictions into our design by effectively using the language features.
Some Quick wins 🍒
-
Make a habit to use
finalbeforevarand function params to guard your references. -
Follow Immutable strategy for POJOs from Oracle’s Documentation
-
(Pre Java 16) Auto-generate Immutable version of your POJO using:
- Lombok (More Magic, Less Effort)
- Google Auto (Less Magic, More Effort)
- Immutables (Less Magic, More Effort)
-
Use
Recordtypes from Java 16
Anti-Immutables
Some prevailing arguments about Immutability
Isn’t Immutability only for Multi-threading?
- Whenever I try to convince these old school java programmers, who are conditioned to mutability, I get this question a lot: “But isn’t immutability just for multi-threading? Why do I need it if my app is Single Threaded?”
- Let me remind you, the app running on your machine may be single-threaded, but the one running in your skull is not.

- It’s by default concurrent, with all the distractions. Plus, it doesn’t come with a built-in debugger.
- With mutable objects, you need to build-up all that state in your head, and a simple distraction can puff it all up. Immutability eliminates that, by-definition.
- Hope that answers this question.
Immutable Objects doesn’t fit my Imperative style?
Mutation and imperative are super good friends, and one likes to be with the other.
void mutableFn() { var mutableList = Arrays.asList("a", "b", "c"); mutateList(mutableList);}
List<String> mutateList(List<String> list) { for (var i = 0; i < list.size(); i++) { list.set(i, list.get(i).toUpperCase()); } return list;}But if you use Immutable objects, you need to replace your Imperative mutations with Declarative transformations.
void immutableFn() { final var immutableList = List.of("a", "b", "c"); transformList(immutableList);}
List<String> transformList(final List<String> list) { // `toList()` is new in Java 16 to collect Stream into UnmodifiableList. return list.stream().map(String::toUpperCase).toList();}- Immutability and Transformations are like Couple. They ought to live together, no choice! ;)
- If you try to perform mutation on an immutable object, you get an exception right on your face
void immutableFn() { final var immutableList = List.of("a", "b", "c"); // ! Throws UnsupportedOperationException ⛔️ mutateList(immutableList);}Immutability forces Transformation (Now I don’t have to tell you, who is the wife and who is the husband! 😉)
Doesn’t Immutability affect Perf?
-
Immutability may lead to creating more objects. But let’s see what Oracle says:
-
ctrl-c + ctrl-v from Oracle’s Documentation
- The impact of object creation is often overestimated.
- It can be offset by decreased overhead due to garbage collection.
-
Period!
Java’s embracing Immutability, slowly
- Most used Data type in any Java application?
String, No coincidence that it’s Immutable. - Java 8 replaced
Datewith immutableLocalDate. - Java 11 introduced Immutable Collections.
- Java 16 introduced
Recordtypes and a concise Stream operationtoListfor UnmodifiableList.
Any many more. The tide’s turning! 🌊
My Talks on this
- 🇪🇸 JBCN Conf, 2021, Barcelona, Spain.
- 🇪🇺 jLove, 2021, Europe.
- 🇮🇳 Functional Conf, 2022, India.