Skip to main content
Create your own
Lesson illustration

Immutable Objects in Concurrent Systems

Hello! Welcome to your next lesson in our journey through fundamental design principles.

In our last session, we discussed the importance of designing clear and stable APIs. A key part of that stability comes from how you handle the data that flows through the API. The Data Transfer Objects (DTOs) you use in your Spring Boot applications are a prime example. If the state of these objects can be changed unexpectedly, it can lead to subtle bugs and unpredictable behavior.

Today, we'll explore a powerful concept that directly addresses this challenge: immutability. By making objects immutable, we can guarantee that their state will never change after they are created. This simple guarantee has profound benefits for code safety, clarity, and performance.

Our learning outcome for this lesson is to: Define the concept of an immutable object and its benefits in concurrent systems. We'll cover what immutability is, why it's so beneficial (especially for the concurrent, multi-threaded environments common in backend services), and exactly how to create immutable classes in Java.

1. What is an Immutable Object?

At its core, an immutable object is an object whose internal state cannot be modified once it has been created. Any operation that appears to "change" an immutable object actually returns a new object with the new state, leaving the original untouched.

The String class in Java is a perfect, everyday example. Any method you call on a String object, like toUpperCase() or substring(), doesn't alter the original string; it creates and returns a new one.

To get a more intuitive feel for this, let's watch a short video that explains the concept visually.

Immutability, visually explained | Code Words

The video 'Immutability, visually explained' by Jordan West provides an excellent, easy-to-understand introduction to the concept.

Watch the first minute of the video (00:00 - 01:02). It uses a simple analogy of editing a novel to contrast mutable (changing the original) and immutable (creating a new copy) operations.

This idea of creating a new copy instead of modifying the original is the fundamental principle of immutability.

2. The Benefits of Immutability

Why go to the trouble of creating new objects instead of just changing existing ones? The benefits are significant, especially in the context of system design and building robust backend services.

The Biggest Win: Thread Safety

In your work with Spring Boot, your application is likely handling multiple requests simultaneously, each in its own thread. When multiple threads access and modify the same mutable object, you can run into serious problems like race conditions. This is where the final state of the object depends on the unpredictable order in which threads are scheduled, leading to corrupted data and bugs that are notoriously hard to reproduce and fix.

Immutable objects solve this problem elegantly. Since they can't be changed, they can be shared freely and safely across multiple threads without any need for synchronization (synchronized blocks, locks, etc.). There are no race conditions because there is no mutable state to race for.

This next segment of the video we started provides a fantastic visual demonstration of this exact problem and its solution.

Immutability, visually explained | Code Words

Let's continue with 'Immutability, visually explained' to see how immutability prevents race conditions in concurrent systems.

Watch from 02:54 to 04:53. Pay close attention to: How two threads trying to increment a shared, mutable counter can produce an incorrect result (the race condition). How using an immutable approach, where each thread creates a new value, completely avoids the issue.

Other Key Advantages

Beyond thread safety, immutability offers several other benefits that lead to cleaner, more maintainable code.

Why is String Immutable in Java?
This infographic on Java's `String` class highlights several key benefits of immutability that apply to any immutable object, such as security, memory efficiency, and predictability.

Here's a summary of the main advantages, which are also concisely listed in the article "Java Immutability top Interview questions.":

  • Predictability and Simplicity: You can pass an immutable object to any method, confident that it won't be changed. This makes your code much easier to reason about and debug. Your objects have a consistent, reliable state.
  • Security: If an object contains sensitive information (like configuration or credentials), making it immutable prevents it from being maliciously or accidentally altered after creation.
  • Cacheability: Immutable objects are excellent candidates for caching. Since their value never changes, a cached instance will never become stale.
  • Safe HashMap Keys: They make great keys for a HashMap. The hashCode() of an object is used to determine which bucket it goes into. If a key object were mutable and its state changed after being inserted, its hashCode() would likely change too. You might not be able to retrieve the value because the map would look in the wrong bucket. Immutability prevents this entirely.

3. How to Create an Immutable Class in Java

Now for the practical part. As a Java developer, you need to know the specific rules for creating a truly immutable class. Just marking a field final is often not enough.

The rules are:

  1. Declare the class as final to prevent subclasses from overriding methods and introducing mutability.
  2. Make all fields private and final. private prevents direct access, and final ensures they are only assigned once in the constructor.
  3. Do not provide any "setter" or other methods that modify the state.
  4. Initialize all fields in the constructor.
  5. Perform defensive copies for any mutable fields. This is the most critical and often-missed rule. If your class holds a reference to a mutable object (like a Date or a HashMap), you must create copies of it in both the constructor and any getters.

Let's watch a video that demonstrates these rules, paying special attention to the pitfall of not handling mutable fields correctly.

Immutable Classes and Objects in Java

The video 'Immutable Classes and Objects in Java' from Neso Academy provides a step-by-step walkthrough of creating an immutable class and highlights a common mistake.

Watch from the beginning to 04:51. This covers: The definition of an immutable class. The core rules for immutability. A simple correct example (C1). An incorrect example with a setter (C2). The crucial part: why class C1 becomes mutable when it contains a reference to the mutable C2 object and exposes it through a getter.

Deep Dive: Defensive Copying with Collections

The video showed the danger of exposing a reference to a mutable object. This is especially important when dealing with collections like List or Map.

The following article provides excellent code examples that show exactly how to handle a HashMap field correctly, and what happens when you don't.

How to Create an Immutable Class in Java

Let's read the article 'How to Create an Immutable Class in Java' from DigitalOcean. It provides concrete Java code to solidify your understanding of defensive copying.

Read the sections 'Creating an Immutable Class in Java', 'What happens when you don’t use deep copy and cloning', and 'Interface Design Pitfalls That Break Immutability'. Focus on: The first code example (FinalClassExample.java) which correctly performs a deep copy in the constructor and clones the map in the getter. The second example, which shows how modifying the code to use shallow copy breaks immutability. The output clearly shows the internal state being changed from outside the class. The discussion on pitfalls, especially returning a mutable object from a getter.

To summarize the key takeaways from the article on defensive copying:

  • In the constructor: When a mutable object (like a HashMap) is passed in, create a new HashMap and copy the contents from the input map to your internal field. This prevents the caller from changing your object's state after construction.
  • In the getter: When returning a mutable internal object, don't return the reference directly. Instead, return a new copy of it. This prevents the caller from changing your object's internal state. An alternative is to return an unmodifiable view (e.g., Collections.unmodifiableMap()), which will throw an exception if a modification is attempted.
Test your understanding!

You are designing a UserSession class to store user data. It needs to hold the user's ID and a list of their recent activities. The class should be immutable.

Here's a first draft:

import java.util.List;

public final class UserSession {
    private final long userId;
    private final List<String> recentActivities;

    public UserSession(long userId, List<String> recentActivities) {
        this.userId = userId;
        this.recentActivities = recentActivities;
    }

    public long getUserId() {
        return userId;
    }

    public List<String> getRecentActivities() {
        return recentActivities;
    }
}

Is this class truly immutable? If not, what changes are needed to make it so?

Show answer

No, this class is not truly immutable. While it follows some of the rules (class is final, fields are private final, no setters), it fails on the crucial point of defensive copying for its mutable recentActivities field (List is a mutable interface).

There are two vulnerabilities:

  1. Constructor Vulnerability: The constructor stores the reference to the List passed in. The caller can modify this list after the UserSession object is created, thus changing the internal state of the session.
  2. Getter Vulnerability: The getRecentActivities() method returns a direct reference to the internal list. The caller can then add, remove, or clear elements from this list, again changing the object's internal state.

How to Fix It:

You need to apply defensive copying in both the constructor and the getter.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class UserSession {
    private final long userId;
    private final List<String> recentActivities;

    public UserSession(long userId, List<String> recentActivities) {
        this.userId = userId;
        // 1. Defensive copy in the constructor
        this.recentActivities = new ArrayList<>(recentActivities);
    }

    public long getUserId() {
        return userId;
    }

    public List<String> getRecentActivities() {
        // 2. Return an unmodifiable view in the getter
        // This is generally more efficient than creating a new copy every time.
        return Collections.unmodifiableList(this.recentActivities);
        // An alternative is to return a new copy:
        // return new ArrayList<>(this.recentActivities);
    }
}

With these changes, the UserSession class is now truly immutable.

Conclusion

In this lesson, we've explored the principle of immutability, a cornerstone of robust and safe software design. Using immutable objects is a key strategy for writing predictable and maintainable code, especially in the complex, concurrent world of backend services.

Key Takeaways:

  • Definition: An immutable object is one whose state cannot be changed after it is created. Operations that seem to modify it actually produce a new instance.
  • Primary Benefit: Immutable objects are inherently thread-safe. They can be shared across threads without synchronization, eliminating the risk of race conditions.
  • Other Benefits: They also lead to more predictable, secure, and simple code, and are ideal for use as cache entries or keys in HashMaps.
  • Java Implementation: To create a truly immutable class, you must not only make the class final and fields private final, but also crucially perform defensive copies of any mutable objects passed to the constructor or returned from getters.

The concept of immutability aligns well with many other design principles. In our next lesson, we will begin our exploration of the SOLID principles, starting with the Single Responsibility Principle (SRP). You'll see that immutable objects often adhere well to SRP; by definition, their responsibility is simply to hold a consistent snapshot of data, not to manage complex state transitions.

Can't find a good explanation? Sign up and we'll make it for you

Sign up