Interview Prep Zoneby CuriouserLabs

Question 3 of 5 · senior level

How do you design an immutable class, and why does immutability matter so much in concurrent systems?

🎤 Say this first

Recipe: final class, all fields private final, no setters, defensive-copy mutable inputs and outputs, and don't leak this from the constructor. Records give you most of it for value carriers. The concurrency payoff is fundamental: immutable objects are thread-safe by construction (no locks, no visibility protocol — final fields have special JMM semantics guaranteeing safe publication), and they make reasoning local: a reference you hold can never change beneath you.

The full picture

The full checklist in one class
public final class Schedule {                         // 1. no subclass mischief
    private final List<LocalDate> dates;               // 2. private final
    private final Owner owner;

    public Schedule(List<LocalDate> dates, Owner owner) {
        this.dates = List.copyOf(dates);               // 3. defensive copy IN
        this.owner = owner;                            //    (Owner itself immutable)
    }
    public List<LocalDate> dates() { return dates; }   // List.copyOf is already
    public Schedule plus(LocalDate d) {                //    unmodifiable — safe OUT
        var next = new ArrayList<>(dates); next.add(d);
        return new Schedule(next, owner);              // 4. "mutators" return new
    }
}
// Records: record Schedule(List<LocalDate> dates, Owner owner) {
//   Schedule { dates = List.copyOf(dates); }          // compact ctor still must copy!
// }
  • JMM guarantee: a properly constructed object's final fields are visible to all threads without synchronization — the only free safe-publication mechanism in Java.
  • Costs & mitigations: allocation churn for 'modified copies' — usually irrelevant (young-gen deaths are cheap), and persistent-structure libraries or builders handle hot cases.
  • Where it matters most: map keys, cache values, messages between threads/actors, configuration snapshots, and anything crossing an API boundary.
  • Records don't make deep immutability automatic — a record holding a mutable List is a shallow shell; copy in the compact constructor.

🔄 Likely follow-up questions

  • Why are String and BigDecimal immutable, and what did that decision buy the platform?
  • Explain how final-field semantics make the singleton double-checked-locking discussion mostly obsolete.
  • When is a mutable builder + immutable product the right pairing?