Java Anti-patterns that Actually Work
Challenging the notion of universally-bad anti-patterns in niche scenarios where they shine
An antipattern is generally a common solution to a recurring problem that seems intuitive or appealing but is considered ineffective or counterproductive in most cases-often leading to technical debt, maintenance issues, or poor performance.
However, some anti-patterns can still "work" under specific circumstances, offering practical benefits when applied judiciously. I’ll share some anti-patterns, explain what it is, why it’s considered an anti-pattern, why it can still work, and when you might use it.
Anti-pattern #1: Busy Waiting (Spin Locks)
Busy Waiting involves repeatedly polling a condition in a loop instead of using proper synchronization mechanisms like wait()/notify()
or locks. Example:
public class ResourceChecker {
private boolean isReady = false;
public void waitForResource() {
while (!isReady) {
// Spin until ready
}
System.out.println("Resource is ready!");
}
public void setReady() {
isReady = true;
}
}
Why is it an Antipattern?
CPU Waste: It consumes CPU cycles unnecessarily, reducing efficiency and scalability.
Unpredictability: It doesn’t handle concurrency well without additional synchronization, risking race conditions.
Better Alternatives: Java provides Thread.sleep(), wait(), or Lock constructs that are more efficient and idiomatic.
Why it Works?
Low Latency: In very short-lived scenarios, busy waiting can respond faster than context-switching with sleep() or wait().
Simplicity: For a single-threaded app or a tiny delay, it avoids the complexity of synchronization primitives.
Control: It gives fine-grained control in niche cases, like real-time systems.
When to Use It?
You’re in a performance-critical, short-duration scenario where the condition will change almost immediately (e.g., waiting for a hardware flag).
You’re prototyping in a single-threaded context and don’t need efficiency (e.g., a game loop waiting for user input).
You’re on a resource-constrained system where threading overhead is prohibitive (rare in Java, but possible).
A better approach uses Thread.sleep()
or wait()
:
public void waitForResource() throws InterruptedException {
while (!isReady) {
Thread.sleep(10); // Less CPU-intensive
}
}
But busy waiting "works" when latency trumps efficiency.
Anti-pattern #2: Null as a Return Value
Returning null from a method to indicate "no result" or "failure" instead of using alternatives like exceptions,Optional, or meaningful objects. Example:
public class UserService {
public String getUserName(int id) {
if (id == 1) {
return "Alice";
}
return null; // No user found
}
public void printUser(int id) {
String name = getUserName(id);
if (name != null) {
System.out.println("User: " + name);
} else {
System.out.println("User not found");
}
}
}
Why is it an Antipattern?
Null Pointer Exceptions (NPEs): Callers must always check for
null
, and forgetting to do so leads to runtime crashes.Ambiguity:
null
doesn’t convey why no result was returned (e.g., not found vs. error).Modern Alternatives: Java 8’s
Optional
or throwing exceptions provide clearer, type-safe ways to handle absence.
Why it Works?
Simplicity: In small, controlled codebases, returning
null
is quick to implement and easy to understand.Legacy Compatibility: Older APIs or libraries often use
null
, so sticking with it avoids conversion overhead.Performance: Avoiding
Optional
or exception creation can be slightly more efficient in tight loops or trivial cases.
When to Use It?
You’re working in a small, self-contained app where null checks are manageable and NPEs are unlikely (e.g., a script with few callers).
You’re interfacing with legacy code that expects null (e.g., an old framework).
The method’s context makes null’s meaning obvious, and performance is critical (e.g., a cache lookup returning null for "not cached").
A better approach uses Optional:
public Optional<String> getUserName(int id) {
if (id == 1) {
return Optional.of("Alice");
}
return Optional.empty();
}
But null "works" when simplicity outweighs robustness.
Anti-pattern #3: Poltergeist (Short-Lived Objects)
Creating objects that exist only briefly and mainly delegate functionality to other objects.
Why is it an anti-pattern?
Unnecessary object creation increases memory usage and garbage collection overhead.
Violates Encapsulation and Object-Oriented Design principles.
Code becomes harder to trace and debug.
Why does it work?
Helps in loosely coupling components, making refactoring easier.
Reduces code duplication by creating helper objects for temporary operations.
Useful for lambda-based or functional programming approaches.
When to use it?
When designing decorators, proxies, or event-driven systems.
When temporary objects improve code readability.
When working with Java Streams and Lambdas.
public class DataProcessor {
public static void processData(String input) {
new Validator(input).validate(); // Poltergeist Object
}
}
class Validator {
private String data;
public Validator(String data) {
this.data = data;
}
public void validate() {
System.out.println("Validating: " + data);
}
}
This class exists only for a moment, then disappears - useful in transient operations.
Anti-pattern #4: Overusing Exceptions for Flow Control
Using exceptions to manage normal program flow instead of conditional logic.
public class Parser {
public int parseNumber(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
return 0; // Default value
}
}
}
Why is it an Antipattern?
Performance: Throwing and catching exceptions is significantly slower than simple if checks due to stack trace generation.
Semantics: Exceptions are meant for exceptional cases, not routine logic, making the code harder to follow.
Debugging: It obscures the distinction between errors and expected behavior, complicating maintenance.
Why it Works?
Conciseness: It can reduce boilerplate if-else blocks, making trivial cases cleaner.
Built-in Handling: Leverages Java’s exception system to handle edge cases without extra validation code.
Fail-Safe: In non-critical apps, it ensures the program keeps running instead of failing outright.
When to Use It?
You’re in a quick-and-dirty script where invalid input is rare and a default value is acceptable (e.g., parsing optional config).
The exception case is truly exceptional but benign, and performance isn’t critical (e.g., a one-time CLI tool).
You’re working with an API that throws exceptions as its primary feedback mechanism (e.g., some legacy parsers).
Better practice:
public int parseNumber(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
// Log error or throw a custom exception
throw new IllegalArgumentException("Invalid number: " + input);
}
}
But the antipattern "works" for rapid, low-stakes coding.
Anti-pattern #5: Public Fields
Exposing class fields as public
instead of using private fields with getters and setters.
public class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
Usage
Person p = new Person("Bob", 30);
p.name = "Alice"; // Direct modification
Why is it an Antipattern?
Encapsulation Violation: Direct access prevents validation, invariants, or future changes to internal representation.
Fragility: Callers depend on the field’s existence and type, breaking if the class evolves.
Security: No control over who modifies the data or how.
Why it Works?
Simplicity: For small, internal data structures, it eliminates getter/setter boilerplate, making code shorter.
Performance: Direct field access is slightly faster than method calls (though JIT optimization often negates this).
Clarity: In trivial cases, it’s obvious what the fields represent without extra abstraction.
When to Use It?
You’re defining a simple DTO (Data Transfer Object) or struct-like class for internal use with no business logic (e.g., a point in a game).
You’re in a prototype or single-developer project where encapsulation isn’t a priority.
The class is immutable in practice (e.g., all fields are set in the constructor and never changed).
Better practice:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
}
But public fields "work" for quick, informal structs.
Anti-pattern #6: String Concatenation in Loops
Building strings by repeatedly concatenating them in a loop using the +
operator, instead of using StringBuilder
or StringBuffer
public class LogGenerator {
public String generateLog(List<String> events) {
String log = "";
for (String event : events) {
log += event + "\n";
}
return log;
}
}
Why is it an Antipattern?
Performance: Each
+
operation creates a new String object (since strings are immutable), leading toO(n²)
time complexity and excessive memory usage.Garbage Collection Overhead: The discarded intermediate strings pile up, stressing the GC in large loops.
Best Practice:
StringBuilder
is explicitly designed for this, offeringO(n)
performance.
Why it Works?
Readability: For small datasets, the code is more intuitive and concise than setting up a
StringBuilder
.Negligible Impact: With a handful of iterations (e.g., <10), the performance hit is imperceptible.
Quick Fixes: In a pinch, it gets the job done without extra class setup.
When to Use It?
You’re dealing with a tiny, fixed-size loop where performance isn’t a concern (e.g., concatenating 3-5 status messages).
You’re prototyping or debugging, and the code won’t live long enough to matter.
You’re in a context where memory and CPU are abundant, and readability trumps micro-optimization.
For production, use:
public String generateLog(List<String> events) {
StringBuilder log = new StringBuilder();
for (String event : events) {
log.append(event).append("\n");
}
return log.toString();
}
But concatenation "works" for small-scale tasks.
These anti-patterns are generally bad practice, but they work in the right context. They shine in niche, low-stakes scenarios but falter under scrutiny in production systems.