StringBuilder vs StringBuffer — ThreadLocal Pollution Fix
ThreadLocal<StringBuilder> polluted log entries across requests.
- StringBuilder and StringBuffer are mutable character sequences for building strings without creating intermediate objects.
- StringBuilder is unsynchronized, faster, and the default choice for single-threaded code (99% of use cases).
- StringBuffer synchronizes every method, providing thread safety at a 3-4x performance cost.
- Pre-size the capacity if you know approximate final length to avoid costly array copies during resizing.
- Both share nearly identical API: append, insert, delete, reverse, toString – only the synchronization differs.
Imagine you're writing a group shopping list on a whiteboard. String in Java is like writing on a sticky note — every time you add an item, you throw the note away and write a brand new one. StringBuilder is like that whiteboard — you just add to it directly. StringBuffer is the same whiteboard, but with a lock on it so only one person can write at a time, preventing chaos when multiple people try to edit it simultaneously.
Every Java application that builds dynamic text — log messages, SQL queries, JSON payloads, HTML templates — ends up concatenating strings. Doing that naively with the + operator is one of the most common silent performance killers in Java code. Senior developers spot it in code reviews immediately, and fixing it can shave milliseconds off hot paths that run millions of times a day.
The problem is that Java's String class is immutable. Every time you concatenate two strings, Java secretly creates a brand new String object in memory and throws the old one away. Do that inside a loop a thousand times and you've just created a thousand objects for the garbage collector to clean up. StringBuilder and StringBuffer exist specifically to solve this — they let you build up a string piece by piece in one mutable buffer, then convert to an immutable String only once at the end.
By the end of this article you'll understand not just how to use StringBuilder and StringBuffer, but why they exist, what makes them fundamentally different from each other, when to choose one over the other, and the exact mistakes that trip up developers in interviews and production code alike.
Why String Concatenation in Loops is a Silent Performance Trap
Before we talk about the solution, you need to feel the pain of the problem. In Java, String objects are immutable — once created, their content never changes. That sounds harmless until you write a loop.
When you write result = result + word inside a loop, the JVM doesn't append word to the existing string. It allocates a fresh String object that holds the combined content, then makes result point to that new object. The old one becomes garbage. Run that 10,000 times and you've created 10,000 short-lived String objects — all to build one final string.
Modern JVMs do optimise simple, single-line concatenations using StringBuilder under the hood (thanks to the javac compiler). But inside a loop, the compiler can't reliably collapse those operations, so you're on your own.
This is the exact reason StringBuilder was introduced in Java 5. It maintains an internal char[] array that grows dynamically. Every call writes into that array — no new object, no garbage. You only pay the cost of creating a String once, at the very end when you call append()toString().
"Hello" + " " + "World" with a StringBuilder call — but only for compile-time constant expressions. Inside a loop, it creates a new StringBuilder on every iteration, which defeats the purpose. Always create ONE StringBuilder before the loop.StringBuilder Deep Dive — The Right Tool for Single-Threaded Work
StringBuilder is the class you'll reach for 95% of the time. It's fast, simple, and lives in java.lang so no import is needed. Let's understand it properly rather than just knowing exists.append()
Under the hood, StringBuilder allocates a char[] with a default capacity of 16 characters. When you append and exceed that capacity, it automatically doubles the array size and copies the content over. If you already know roughly how long your final string will be, pass that as an initial capacity — it eliminates those costly resize operations entirely.
The API is designed with method chaining in mind. Every method that modifies content returns this, so you can chain , append(), insert(), and delete() calls fluidly on a single line.replace()
One underused feature is — it lets you splice content into the middle of what you've already built, which is incredibly useful for building things like template strings or protocol messages where the length field comes before the body but is only known after you've written the body.insert()
StringBuffer — Why Thread Safety Has a Price
StringBuffer is StringBuilder's older sibling. It was introduced in Java 1.0, a full four years before StringBuilder arrived in Java 5. The API is almost identical — you get the same , append(), insert(), delete(), and reverse()toString() methods. The one fundamental difference is that every mutating method in StringBuffer is declared synchronized.
Synchronization means the JVM places a monitor lock on the StringBuffer object before executing each method. If two threads call append() at the same moment, one of them waits until the other is done. This prevents the kind of data corruption you'd get if two threads tried to resize the internal char[] simultaneously.
But locks are expensive. Even with no contention — when only one thread is actually using the buffer — the JVM still has to acquire and release the lock on every single method call. That overhead adds up fast in tight loops.
The honest truth? StringBuffer is almost never the right answer today. If you need thread-safe string building, you usually need higher-level coordination anyway — producing the string in one thread and passing it to another, or using a thread-local StringBuilder. StringBuffer's fine-grained method-level locking rarely matches real concurrency requirements.
ThreadLocal<StringBuilder>) or have each thread build its own string and combine at the end. StringBuffer's per-method locks don't protect multi-step workflows anyway.Real-World Patterns — When to Actually Use Each
Theory is great, but let's talk about the situations you'll actually encounter on the job. Knowing which tool to reach for without having to think about it is what separates a junior from a senior.
Use StringBuilder whenever you're building a string dynamically in a single thread — which covers building SQL fragments, constructing log messages, assembling file paths, generating HTML or CSV content in a loop, or implementing a toString() method on a complex object.
Use StringBuffer almost never — but legitimately when a buffer truly must be written to by multiple threads concurrently and each individual call is the complete unit of work. Legacy codebases using Java 1.4 or earlier often use it throughout.append()
Use plain String concatenation freely for simple, readable two- or three-part joins outside any loop. The compiler handles those efficiently. Readability wins there.
The pattern below shows a real-world toString() implementation — something every Java developer writes constantly — done correctly with StringBuilder.
String.join() before StringBuilder. They eliminate the 'trailing delimiter' bug entirely — the one where you accidentally end up with 'Alice,Bob,Charlie,' and can't figure out why.String.join() is cleaner and safer.Performance Tuning: Choosing the Right Initial Capacity
The default capacity of StringBuilder (and StringBuffer) is 16 characters. If you append more than that, the internal array is doubled and copied. This resizing is O(n) per copy, and it happens multiple times as the buffer grows. If you know your final string will be around 200 characters, the buffer will resize 4 times (16→32→64→128→256). Each resize copies all existing content.
Providing an initial capacity that matches your expected output eliminates all resizing. This is a micro-optimization, but on hot paths it matters. Use new StringBuilder(expectedLength) where expectedLength is a reasonable upper bound.
But be careful: over-sizing wastes memory. If you set capacity to 10,000 but only append 100 characters, you've allocated a 10,000-char array. For short-lived builders this is fine, but for long-lived ones it's wasteful.
A good rule: use expected string length as capacity. If you're not sure, 256 is a safe default for most logs and messages. You can also use ensureCapacity(int) after construction if you discover a larger size is needed.
- Capacity is the allocated memory size (char[] length).
- Length is the number of characters currently stored.
- When length == capacity, the kitchen needs to expand — doubling is the default.
- Expansion creates a new char[] and copies old data — O(n) per resize.
- Setting initial capacity to expected final length avoids the resize tax entirely.
new StringBuilder(1024) eliminated a resize per log line — reduced CPU usage by 8% across the cluster.Thread-Local StringBuilder Shared Across Requests Caused Data Corruption in Logging Pipeline
- ThreadLocal does not guarantee isolation across tasks if you don't reset state between uses.
- StringBuilder is safe when it's truly thread-confined — verify your concurrency model before reusing any mutable object.
- StringBuffer's synchronized methods wouldn't have helped here either; the issue wasn't concurrent writes but stale content. The real fix is lifecycle management, not thread safety.
Key takeaways
String.join() for simple cases — eliminate entire categories of delimiter bugs and should be in your muscle memory.Common mistakes to avoid
3 patternsCreating a new StringBuilder inside a loop
Assuming StringBuffer makes multi-step operations atomic
buf.length() < max) buf.append(data) still has a race.Using StringBuilder's toString() result before you're done building
Interview Questions on This Topic
What is the difference between String, StringBuilder, and StringBuffer in Java, and how would you decide which one to use in a given situation?
Frequently Asked Questions
That's Strings. Mark it forged?
5 min read · try the examples if you haven't