Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Immutable Collections for Safer Concurrency

Posted on April 17, 2009 By Luis Fernandez

Your code is not broken. It is just being poked at the same time by two or five threads. These things used to be rare when we had one real core and dreams. Now your laptop ships with two or four cores and your server feels like a small city. If you keep passing around mutable collections, you are making every thread share the same toy and then you wonder why the wheels fall off. Let’s talk about immutable collections and why they are the quiet heroes of safer concurrency in Java.

Why immutability calms down your threads

In Java, an object that never changes after construction is immutable. No setters. No methods that mutate internal state. No fields that can be reassigned after the constructor finishes. This one idea removes a whole family of bugs. If nothing can change, there is nothing to race. No need to guess if you synchronized in the right place. No need to keep track of who owns the lock. And you can share the object freely across threads without fear.

The JVM gives us a strong memory model from Java 5 onward. If you set all fields as final, and you build the object in one thread then publish it to other threads, those threads see a fully constructed object. This is not just folklore. The rules say that final fields have special guarantees at the end of the constructor. That means visibility is handled for you. Combine that with the fact that immutable objects never change and you have safe publication with almost no ceremony.

Collections are the tricky part. Lists, maps, sets. They are the things we pass around most. A mutable list shared across threads is a slot machine with your data as the coins. You can wrap it with synchronized calls, you can guard it with locks, but you will still be nervous. Immutable collections remove that nervousness. Build once, share everywhere, never worry again.

What Java offers today and how to use it

Out of the box you have two main options. The first is Collections.unmodifiableX. The second is to build your own small immutable types that hold these collections. There is also a very helpful library from Google called Google Collections that already ships with ImmutableList and friends. We will use all three approaches with a clear rule in mind. An unmodifiable view is not the same as an immutable object.

Here is a common pitfall with the standard library wrappers:

List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");

List<String> shared = Collections.unmodifiableList(names);

// Somewhere else
names.add("Grace"); // Oops, shared sees this change

The wrapper stops callers from adding through the wrapper, but it does not stop the original list from changing. To get real immutability, make a defensive copy before you wrap:

List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");

// Defensive copy for real immutability
List<String> shared = Collections.unmodifiableList(new ArrayList<>(names));

// Drop the original reference or keep it private
names = null; // optional

This pattern is simple and effective. Build privately, copy, wrap, and share the wrapped copy. If you do this inside an immutable holder object with final fields, you get easy safe publication:

public final class Team {
  private final List<String> members;

  public Team(List<String> input) {
    // copy then wrap
    this.members = Collections.unmodifiableList(new ArrayList<>(input));
  }

  public List<String> members() {
    return members; // safe to share
  }
}

If you can add a third party library, the Google Collections project makes this cleaner. It has ImmutableList, ImmutableSet, and ImmutableMap with builders that copy by default and give you truly immutable results.

// Google Collections
ImmutableList<String> names = ImmutableList.of("Ada", "Linus", "Grace");

ImmutableMap<String, Integer> ages = new ImmutableMap.Builder<String, Integer>()
    .put("Ada", 36)
    .put("Linus", 39)
    .build();

// Safe to share across threads without locks

All of these choices share the same spirit. Build privately. Freeze the result. Publish as final. When you need to change something, do not mutate the old list. Create a new one and publish that new instance instead.

Working with concurrency without drama

Let’s say you have a cache of configuration values. The default reaction is to reach for a concurrent map and mutate it on the fly. That works for some cases, but it also invites state to change under the feet of readers. There is another pattern that thrives with immutability. Use one immutable map and swap the reference when you refresh.

public final class Config {
  private volatile ImmutableMap<String, String> values;

  public Config(ImmutableMap<String, String> initial) {
    this.values = initial;
  }

  public String get(String key) {
    return values.get(key);
  }

  // called by a single updater thread
  public void reload(Map<String, String> fresh) {
    this.values = new ImmutableMap.Builder<String, String>()
        .putAll(fresh)
        .build();
  }
}

Readers never lock. Writers build a fresh map off to the side, then swap the volatile reference. The swap gives visibility to readers and nobody ever sees a half baked state. You can do the same with lists or sets. The only trick is to make the holder reference volatile or publish through some other safe mechanism.

Event listeners are a classic case. Many systems keep a set of listeners and call them from multiple threads. With a mutable set you get locks all over the place or you risk concurrent modification. With an immutable list you can do this:

public final class Dispatcher {
  private volatile ImmutableList<Listener> listeners = ImmutableList.of();

  public void add(Listener l) {
    // Create a new list with the added listener
    listeners = new ImmutableList.Builder<Listener>()
        .addAll(listeners)
        .add(l)
        .build();
  }

  public void remove(Listener l) {
    ImmutableList.Builder<Listener> b = new ImmutableList.Builder<>();
    for (Listener cur : listeners) {
      if (!cur.equals(l)) b.add(cur);
    }
    listeners = b.build();
  }

  public void fire(Event e) {
    for (Listener l : listeners) {
      l.onEvent(e);
    }
  }
}

No locks during dispatch. No concurrent modification. All readers see a stable snapshot. Writers pay the cost of building a new list, which is often fine because listener sets are small and updates are rare compared to reads.

Where immutable collections shine and where they do not

Let’s compare options that people usually pick in concurrent code. This is not about one tool being always right. It is about picking the simplest thing that works for your traffic pattern and your team.

Collections.synchronizedX. Easy to add. Locks on every operation. You can still get iteration failures unless you remember to hold the lock during traversal. That is easy to forget and the code tends to grow locks over time. Good for tiny code paths with low contention. Painful under load.

ConcurrentHashMap. Great when you truly need concurrent updates and high read volume. You still have to think about compound actions. Put if absent is safe with dedicated calls. But anything that depends on two steps can still interleave in surprising ways. You will write more code to guard those cases.

Copy on write collections such as CopyOnWriteArrayList. Excellent for the listener case. Reads are lock free. Writes copy the whole array. That cost is fine if writes are rare. You still get a mutable type that leaks if you hand it out directly.

Immutable collections with reference swaps. Reads are simple and fast. There is no locking in the hot path. Updates are atomic at the reference level and do not affect current readers. Perfect when your data changes in chunks and reads dominate writes. Not a match for very large collections with frequent updates where copying would be expensive.

If you are building a new service and you are staring at shared data structures, start with the immutable approach. If you hit a performance wall, measure and then move the hot spot to a concurrent structure. In many cases you will not need to move at all.

Practical checklist for safer Java concurrency with immutable collections

  • Prefer immutable objects. Make fields final. No setters. Build complete state in the constructor.
  • Freeze collections before sharing. Use defensive copies with Collections.unmodifiableX or use Google Collections ImmutableX builders.
  • Do not leak internals. Never return a mutable reference to internal state. If you must return a collection, return an unmodifiable or immutable view.
  • Safe publication. Hold your immutable data in final fields or publish through a volatile reference or a thread safe container during handoff.
  • Update by replacement. When state changes, build a new immutable instance and swap the reference. Do not mutate in place.
  • Snapshot iteration. Iterate over immutable snapshots for events and reads. No locks needed. No concurrent modification headaches.
  • Watch the size. Immutable swaps are perfect for small to medium collections and read heavy traffic. If your collection is huge and updates are frequent, consider ConcurrentHashMap or a more targeted structure for the hotspot.
  • Be clear in your API. Use naming like asImmutable or toImmutable to signal intent. Keep constructors private if you need factory control.
  • No lazy fields inside immutable objects. If you have a cache inside, make it final and immutable as well, or accept that you are now mutable and guard it.
  • Test for sharing bugs. Write tests that run operations from multiple threads against your public API. If your types are truly immutable, these tests tend to be boring, which is the point.

A small grab bag of patterns and code you can paste today

Static factories that return immutable copies:

public final class Users {
  private final ImmutableMap<String, User> byId;

  private Users(ImmutableMap<String, User> byId) {
    this.byId = byId;
  }

  public static Users from(Collection<User> input) {
    ImmutableMap.Builder<String, User> b = new ImmutableMap.Builder<>();
    for (User u : input) b.put(u.id(), u);
    return new Users(b.build());
  }

  public User get(String id) {
    return byId.get(id);
  }

  public Set<String> ids() {
    return byId.keySet();
  }
}

Defensive copies for method parameters:

public final class Report {
  private final List<Item> items;

  public Report(List<Item> items) {
    // do not trust callers
    this.items = Collections.unmodifiableList(new ArrayList<>(items));
  }

  public List<Item> items() { return items; }
}

Rolling updates with a volatile snapshot:

public final class RoutingTable {
  private volatile ImmutableMap<String, String> route = ImmutableMap.of();

  public String nextHop(String key) {
    return route.get(key);
  }

  public void refresh(Map<String, String> newData) {
    route = new ImmutableMap.Builder<String, String>().putAll(newData).build();
  }
}

These patterns show the same story. Build privately. Freeze. Publish safely. Treat updates as new objects, not mutations.

One last detail. If you rely on Collections.unmodifiableX, remember that it is a view. The moment you hand the original mutable instance to someone else, your immutability is gone. Either never share the original or switch to a library that provides a true immutable type that copies on build.

Why talk about this right now. Because we are all moving on to more cores and we cannot keep sprinkling synchronized everywhere. You do not want to turn your codebase into a museum of locks. It is much nicer to structure your data as immutable snapshots that you can reason about in your head.

Java gives us everything we need for this today. The memory model is solid. The standard library covers the basics. Google Collections brings a friendly immutable family that plays well with the JDK. And the mental model is simple enough to explain to the team in one whiteboard session. That is a win in my book.

Immutable collections make concurrent code boring. Boring is good. Boring is stable. Boring lets you ship.

General Software Software Engineering

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes