Skip to main content

🧠 Java Production Trap I’ll Never Forget – Let’s Talk About ConcurrentModificationException

 

πŸ’¬ “When I was a fresher, I used to wonder: what things should I never do in Java?”

That curiosity led me to explore tons of weird bugs and war stories people had faced in production. Some were scary, some silly — but all of them were super valuable to understand deeply.

Today, I’m sharing one such issue I came across.

⚠️ P.S. I didn’t write this code — but I sure learned a lot digging into what went wrong and how it was fixed. πŸ˜…

This post is for the curious devs out there — especially freshers who want to learn the "why" behind Java's behaviors, not just memorize "what to do".


πŸ’₯ The Problem: ConcurrentModificationException in a Simple Loop

πŸ“Environment:
This happened in a Spring Boot microservice deployed on OpenShift (Linux-based). Interestingly, it didn’t throw any error in the developer's local Windows machine during testing. But in prod? BOOM. πŸ’£

Here’s the actual buggy code snippet:


List<String> users = getActiveUsers(); // Fetch from DB for (String user : users) { if (user.startsWith("temp_")) { users.remove(user); // 🚨 This caused the exception } }

And here’s the ugly surprise from the logs:


Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)

❓My Doubt: “Why ‘Concurrent’ if I'm Not Using Threads?”

That word "Concurrent" was super misleading to me back then.
I wasn’t using any threads! It was just a simple for-loop and a list.

So I started digging:
What does this error really mean? When and why does it happen?


πŸ”¬ Root Cause Analysis (RCA)

Here’s the twist: even though it looks like you’re looping normally, Java’s for-each loop actually uses an Iterator under the hood.

Let’s break it down:

  • Java’s ArrayList maintains an internal mod count (modCount) to track structural changes.

  • When you iterate using for (item : list), it’s essentially doing this:


Iterator<String> it = users.iterator(); while (it.hasNext()) { String user = it.next(); // Your logic here }
  • Now, when you do users.remove(user), you're modifying the list directly, while the iterator is still running.

  • The iterator notices that the list changed without using its own remove() method — and throws ConcurrentModificationException as a fail-fast mechanism.


πŸ› ️ The Fix: Remove Safely Without Angering the Iterator Gods πŸ˜…

Approach 1: Use Iterator Properly


List<String> users = getActiveUsers(); Iterator<String> it = users.iterator(); while (it.hasNext()) { String user = it.next(); if (user.startsWith("temp_")) { it.remove(); // ✅ Safe removal } }

Approach 2: Use Java Streams to Filter


List<String> users = getActiveUsers() .stream() .filter(u -> !u.startsWith("temp_")) .collect(Collectors.toList());

🧠 Why Does This Work?

Using it.remove() is safe because the iterator knows about the removal and updates its internal state. It’s like you politely told the iterator:

“Hey, I’m removing this one — please adjust your internal counter accordingly.” ☕

In the stream version, you’re not modifying the original list at all — you’re creating a new filtered list, which avoids the mutation problem entirely.



"Whaaatttt, then what does iterator.remove() do internally, and why does that work while list.remove() doesn’t?"


πŸ”¬ What Happens Internally When iterator.remove() is Called?

Time to go inside Java’s ArrayList and Iterator πŸš€

πŸ“Œ Internals of ArrayList.iterator()

Java’s ArrayList uses an internal class called Itr as its iterator.

Let’s see its key variables:


int cursor; // index of next element to return int lastRet = -1; // index of last element returned, -1 if none int expectedModCount = modCount; // used for CME detection

πŸ“Œ What .remove() does:

When you call iterator.remove(), it internally calls the parent list’s remove method, but also updates internal state safely.


public void remove() { if (lastRet < 0) throw new IllegalStateException(); ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; // ✅ sync back the modCount }

πŸ’‘ So it does 3 crucial things:

  1. Removes element at correct position (lastRet)

  2. Moves the cursor properly so next iteration continues smoothly

  3. Updates expectedModCount to match modCount so that CME is NOT thrown


🧠 So Why list.remove() (outside iterator) fails?

Let’s say:


List<String> list = new ArrayList<>(List.of("A", "B", "C")); Iterator<String> it = list.iterator();
  1. Internally:

    • modCount = 0

    • expectedModCount = 0

  2. When you call list.remove(1) directly:

    • modCount++ → modCount = 1

    • But expectedModCount in the iterator stays 0

  3. On next it.next(), it checks:


if (modCount != expectedModCount) throw new ConcurrentModificationException();

πŸ’₯ BOOM! That's your exception.



🧊 Final Analogy Time

Think of iterator like a security guard:

  • If you try to remove someone from the party (direct list.remove), the guard freaks out 😱 — CME!

  • But if the guard (iterator.remove) removes them himself, all is cool 😎



😡 Why Didn't It Fail in Local?

This is a classic gotcha.

  • Small dataset in local? Iterator might never throw.

  • JVM behavior or optimizations on your machine might not enforce the check immediately.

  • But in production (large dataset + different JVM or OS), the fail-fast behavior gets enforced reliably.

That’s why the same code works locally but crashes in prod — the worst kind of bug. πŸ˜–


🧩 What I Learned

  • ConcurrentModificationException is not about threads — it’s about modifying collections during iteration in an unsafe way.

  • Always respect the Iterator pattern if you’re doing removals inside a loop.

  • And never assume something is safe just because it runs locally!


🎨 Wrapping Up

I didn’t write this buggy code — but man, I’m glad I found it while exploring “what not to do” in Java. It helped me understand how iteration really works under the hood.

If you're just starting your Java journey, I highly recommend doing this too:

Don’t just learn “how” to fix something — learn why it broke in the first place.

It’ll make you 10x stronger as a dev. πŸš€


πŸ”š Next Weird Bug coming soon!


Got questions or thoughts? Drop them in the comments or connect with me — I love geeking out over stuff like this. πŸ˜„

Comments

Popular posts from this blog

🐱 Tomcat vs ⚡ Netty – Which One Should You Use?

🐱 Tomcat vs ⚡ Netty – Which One Should You Use? So recently I got curious about this too πŸ€”. Everywhere in Spring Boot tutorials we see Tomcat . Then suddenly while exploring Spring WebFlux , the name Netty pops up. And I was like – “Wait, who’s this Netty guy trying to replace Tomcat?” πŸ˜… Let’s break it down with real-time examples , icons , and fun comparisons . 🐱 Tomcat – The Traditional Web Server Type: Servlet Container (blocking I/O) World: Used with Spring MVC Style: Thread-per-request model πŸ‘©‍πŸ’» Pros: Stable, widely used, battle-tested Cons: Struggles with huge concurrent connections πŸ‘‰ Example in real life: Tomcat is like a restaurant with fixed waiters 🍴. - Each customer = one thread/waiter - If too many customers come in at once → waiters run out → customers wait outside πŸšͺ ⚡ Netty – The Reactive Rockstar Type: Asynchronous Event-Driven Network Framework World: Default for Spring WebFlux Style: Event-lo...

🎭 Spring’s Secret: Why @Transactional & Friends Betray You Silently

πŸ’‘ Lesson Learned — Not a Prod Bug, But a Real Pain No, this wasn’t a production outage. Nobody screamed at me. But I sat for 3 hours wondering: “Why the heck is my @Transactional not rolling back!?” 😡‍πŸ’« “Why is Redis cache not working?” 🀯 Turned out, the issue was one silent villain: 🧱 Self-invocation 🀷 What Is @Transactional ? If you're new: @Transactional = Tells Spring to start a DB transaction when a method is called. It’ll commit if everything’s okay. It’ll rollback if something fails. 🧠 Think of it like wrapping your code in: try { beginTransaction(); // your logic commit(); } catch(Exception e) { rollback(); } πŸ•΅️ Real-Life Analogy — The Gateway Community 🏘️ Let me tell you about my society — it has a strict watchman at the gate. Here’s how it works: πŸ›‚ Watchman = Spring Proxy 🏠 Your apartment = Your service class πŸšͺ Your room = A method inside that class πŸƒ Scenario 1: Outsider Visits Your friend from outside...

🌟 My Journey – From Zero to Senior Java Tech Lead 🌟

 There’s one thing I truly believe… If I can become a Java developer, then anyone in the world can. πŸ’― Sounds crazy? Let me take you back. πŸ•“ Back in 2015… I had zero coding knowledge . Not just that — I had no interest in coding either. But life has its own plans. In 2016, I got a chance to move to Bangalore and joined a Java course at a training center. That’s where it all started — Every day, every session made me feel like: "Ohhh! Even I can be a developer!" That course didn’t just teach Java — it gave me confidence . πŸ§ͺ Two Life-Changing Incidents 1️⃣ The Interview That Wasn't Planned Halfway through my course, I had to urgently travel to Chennai to donate blood to a family member. After that emotional rollercoaster, I found myself reflecting on my skills and the future. The next day, as I was preparing for my move to Bangalore to complete the remaining four months of my course, I randomly thought — "Let me test my skills... let me just see...