Definition
Decision:
- StringBuilder: Default choice. Single-threaded or thread-local building.
- StringBuffer: Only when the exact same builder instance is accessed by multiple threads simultaneously.
- Better alternative: Each thread builds with its own StringBuilder. No sharing, no synchronization.
When do you need a shared whiteboard with a lock (StringBuffer)? Almost never. Give each person their own whiteboard (StringBuilder). Faster and simpler.
Understanding the Concepts
The rare case for StringBuffer
// RARE: shared log buffer (legacy pattern)
StringBuffer sharedLog = new StringBuffer();
// Thread 1: sharedLog.append("TX001 success\n");
// Thread 2: sharedLog.append("TX002 failed\n");
// StringBuffer ensures no data corruption
But modern code uses proper logging frameworks (SLF4J) instead of shared string buffers.
The modern alternative
// Each thread builds independently, merge at the end
List<String> results = accounts.parallelStream()
.map(a -> new StringBuilder()
.append(a.getAccountId()).append(": ").append(a.getBalance())
.toString())
.collect(Collectors.toList());
What should a senior engineer know?
- StringBuffer is legacy. New code should never use it.
- If you see StringBuffer in a PR, ask: "Is this builder actually shared across threads?" Usually, it is not.
- ConcurrentLinkedQueue
is a better thread-safe alternative for collecting strings from multiple threads.
What are common mistakes?
A frequent mistake is using == to compare String content instead of equals().
Wrong approach:
package com.bank.service;
public class AccountValidator {
public boolean validateAccountType(String type) {
// WRONG: == compares references, not content
if (type == "SAVINGS") {
return true;
}
return false;
}
}
Correct approach:
package com.bank.service;
public class AccountValidator {
public boolean validateAccountType(String type) {
// CORRECT: equals() compares content
if ("SAVINGS".equals(type)) { // null-safe: literal first
return true;
}
return false;
}
}
Using the literal on the left side of equals() prevents NullPointerException when the variable is null. This is a standard practice at National Bank of India for all string comparisons in service code.
What are the edge cases?
String behavior has several surprising edge cases that can catch banking developers off guard.
package com.bank.edge;
public class StringEdgeCases {
public static void main(String[] args) {
// Edge case 1: compile-time constant folding
String a = "SAV" + "2001"; // Compile-time constant
String b = "SAV2001";
System.out.println(a == b); // true (same pool entry)
// Edge case 2: variable concatenation is NOT pooled
String prefix = "SAV";
String c = prefix + "2001"; // Runtime concatenation
System.out.println(b == c); // false (different objects)
// Edge case 3: final variable IS a compile-time constant
final String finalPrefix = "SAV";
String d = finalPrefix + "2001"; // Compile-time constant
System.out.println(b == d); // true (same pool entry)
// Edge case 4: substring no longer shares char array (Java 7u6+)
String full = "AccountNumber:SAV2001";
String sub = full.substring(14); // New String, new char array
}
}
true
false
true
The final keyword makes the variable a compile-time constant, which changes how the compiler handles concatenation.
What are common interview follow-up questions?
Interviewers often follow up with these related questions:
- How does this concept apply to a high-throughput banking transaction system?
- What are the performance implications in a production environment?
- How would you debug issues related to this concept?
- What tools would you use to monitor this in production?
- How does this behave differently in Java 8 vs Java 17+?
When answering follow-up questions, always relate your answer back to practical experience. For example, at National Bank of India, Rajesh debugged a production issue related to this concept by using JFR recordings and heap dumps. Providing concrete examples from banking systems shows the interviewer that you understand both the theory and the practice.
Visual Overview
Need string building?
├── Single thread? → StringBuilder (default)
├── Multi-thread, separate builders? → StringBuilder (each thread has its own)
└── Multi-thread, SHARED builder? → StringBuffer (rare, usually wrong design)
Better: thread-local builders + merge
Key Takeaway
Use StringBuilder by default. StringBuffer is needed only when multiple threads share the same builder instance, which is rare and usually a design problem. Modern code uses thread-local builders or proper logging frameworks instead.
One sentence to remember: StringBuilder always, StringBuffer almost never – give each thread its own builder.
---