Programming antipatterns are common mistakes that seem like a good idea but lead to poor code quality, maintainability issues, and performance bottlenecks. Here are some deadly programming antipatterns, why they are bad, how they can cost you, and what you should do instead.
1) The Boolean Trap
Passing multiple boolean flags into a method to change its behavior
Bad Example: Boolean Parameters Controlling Behaviour
public void processOrder(boolean isExpress, boolean includeGiftWrap) {
if (isExpress) {
System.out.println("Processing express order");
} else {
System.out.println("Processing standard order");
}
if (includeGiftWrap) {
System.out.println("Adding gift wrap");
}
}
Why is this bad?
Unclear intent – What does true, false mean in
processOrder(true, false)
?Difficult to scale – Adding new conditions requires adding more boolean parameters.
Encourages misuse – It’s easy to pass arguments in the wrong order.
What to do instead? (Use Enum or Config Objects)
public enum OrderType {
STANDARD, EXPRESS
}
public class OrderConfig {
private final OrderType orderType;
private final boolean includeGiftWrap;
public OrderConfig(OrderType orderType, boolean includeGiftWrap) {
this.orderType = orderType;
this.includeGiftWrap = includeGiftWrap;
}
}
public void processOrder(OrderConfig config) {
if (config.orderType == OrderType.EXPRESS) {
System.out.println("Processing express order");
} else {
System.out.println("Processing standard order");
}
if (config.includeGiftWrap) {
System.out.println("Adding gift wrap");
}
}
Why is this good?
Improves readability –
new OrderConfig(OrderType.EXPRESS, true)
is much clearer.Easier to extend – Additional fields can be added without modifying method signatures.
2) The God Object
A single class that does too much and knows everything
Bad Example: The God Class
public class ApplicationManager {
private Database db;
private Cache cache;
private Logger logger;
public void processOrder() {
// Handling order logic
db.saveOrder();
cache.invalidate();
logger.log("Order processed");
}
public void generateReport() {
// Report logic
}
public void sendEmail() {
// Email sending logic
}
public void manageUsers() {
// User management logic
}
}
Why is this bad?
Violates Single Responsibility Principle (SRP) – Handles too many different concerns.
Hard to maintain – Any change could impact multiple unrelated functionalities.
Testing becomes difficult – Need to mock multiple dependencies.
What to do instead? (Refactor into separate classes)
public class OrderService {
private Database db;
private Cache cache;
public void processOrder() {
db.saveOrder();
cache.invalidate();
}
}
public class LoggingService {
private Logger logger;
public void log(String message) {
logger.log(message);
}
}
Why is this good?
Easier to test – Each class does one thing.
More maintainable – Changes are isolated to specific components.
3) The Hidden Dependency Trap
A method silently depends on global state instead of passing dependencies explicitly
Bad Example: Hidden Dependencies
public class OrderService {
public void processOrder() {
Database.save(); // Where is Database defined?
}
}
Why is this bad?
Difficult to test – If
Database
is a global object, we can't mock it.Unexpected side effects – Changing
Database
affects all usages.Less reusability – This method cannot be used with a different storage system.
What to do instead? (Use Dependency Injection)
public class OrderService {
private final Database database;
public OrderService(Database database) {
this.database = database;
}
public void processOrder() {
database.save();
}
}
Why is this good?
Easier testing – You can inject a mock database for unit tests.
More flexibility – Works with different database implementations.
4) The Over-Engineered Abstraction
Creating too many unnecessary layers of abstraction
Bad Example: Too Many Interfaces and Wrappers
public interface OrderProcessor {
void process(Order order);
}
public class DefaultOrderProcessor implements OrderProcessor {
private final OrderRepository orderRepository;
public DefaultOrderProcessor(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void process(Order order) {
orderRepository.save(order);
}
}
Why is this bad?
Adds unnecessary complexity – This extra layer doesn't add any real value.
Harder to debug – More classes to navigate.
Overkill for simple logic – If there's only one implementation, why have an interface?
What to do instead? (Use Simple, Direct Code)
public class OrderService {
private final OrderRepository orderRepository;
public void process(Order order) {
orderRepository.save(order);
}
}
Why is this good?
Less boilerplate – Avoids unnecessary interfaces.
Easier to read and maintain.
5) The Leaky Cache
Cache that isn’t properly invalidated or bloats over time
Bad Example: Cache That Never Expires
private static final Map<String, User> CACHE = new HashMap<>();
public User getUser(String id) {
if (!CACHE.containsKey(id)) {
CACHE.put(id, fetchUserFromDatabase(id));
}
return CACHE.get(id);
}
Why is this bad?
Memory leaks – This cache keeps growing indefinitely.
Stale data – No mechanism to remove or update outdated entries.
Performance issues – Over time, it slows down due to excessive memory use.
What to do instead? (Use Expiring Caches like Guava)
private static final Cache<String, User> CACHE = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
public User getUser(String id) {
return CACHE.get(id, () -> fetchUserFromDatabase(id));
}
Why is this good?
Auto-expires old data – Prevents memory leaks.
Limits cache size – Avoids unbounded growth.
6) The Misused Singleton
A Singleton is a design pattern ensuring a class has only one instance. While useful for specific scenarios (like a logger or configuration manager), overusing the Singleton can lead to tight coupling, reduced testability, and hidden dependencies.
Bad Example: Singleton for Everything
public class Database {
private static final Database INSTANCE = new Database();
private Database() {}
public static Database getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("Doing something important...");
}
}
Why is this bad?
Hard to test – Can’t inject mocks easily.
Global state issue – Shared state across different parts of the system.
What to do instead? (Use Dependency Injection)
public class Database {
public void save() {
// Save data
}
}
public class OrderService {
private final Database database;
public OrderService(Database database) {
this.database = database;
}
}
Why is this good?
Easier testing – No static state, can use mock databases.
Better flexibility – Works with different DB implementations.\
7) Lava Flow
Lava Flow refers to dead or redundant code that remains in the system because engineers are afraid to remove it (e.g leftover POCs, old logic that is never called anymore). This “lava” accumulates over time, making the codebase more complex.
public class OldPaymentProcess {
public void processPayment() {
// This method is never called anymore,
// but it still lives in the codebase.
}
}
Why It’s a Problem
Confusing: Future developers don’t know if the code is used or not.
Wasted maintenance: Tools may still compile or test this code, slowing down builds or code analysis.
Security risks: Old code might rely on outdated libraries or have vulnerabilities.
How to Improve
Conduct regular refactoring sessions to remove unused or outdated code.
Use code coverage tools or static analysis to identify dead code paths.
Some of the most dangerous antipatterns are subtle, leading to hidden technical debt.
Recognizing and avoiding these anti-patterns is key to writing maintainable, testable, and scalable Java code.
Checkout my Twitter and Instagram accounts, where i frequently share my programming journey.