System Design (LLD): Anti-Patterns & The Null Object Pattern
This comprehensive guide covers two essential low-level design topics: Design Anti-Patterns (common traps that lead to brittle, tightly coupled code) and the Null Object Pattern (a lightweight behavioral design pattern used to eliminate dangerous null checks).
1. Introduction to Anti-Patterns in System Design
While design patterns provide proven, elegant solutions to recurring architectural problems, anti-patterns represent common, deceptive practices that seem convenient initially but eventually lead to technical debt, tight coupling, and high maintenance overhead.
2. Anti-Pattern 1: The God Object
A God Object is a single class or object that assumes an overwhelming number of responsibilities, centralizing most of the application's logic, state, and decision-making.
Real-Life Analogy
The Overworked Receptionist: Imagine an office building where a single receptionist is expected to handle mail delivery, book conference rooms, fix broken plumbing, interview job applicants, and process payroll. If that receptionist takes a day off or collapses from exhaustion, the entire building grinds to a halt.
Code & Architectural Impact
- Tight Coupling: Most classes in the application depend heavily on this single central object.
- Violation of SRP: Violates the Single Responsibility Principle completely.
- Cascading Failures: A bug in one peripheral feature housed inside the God Object can corrupt or crash the entire application.
Note: An orchestration object or a Facade (like a Spring Boot Controller layer) is not necessarily a God Object if its sole responsibility is delegation—routing requests to downstream service layers without holding business logic itself. However, if it performs heavy computations and orchestrates operations inline, it slips into the God Object anti-pattern.
3. Anti-Pattern 2: Spaghetti Code
Spaghetti Code describes an application structure characterized by convoluted, unmanaged control flows, circular dependencies, and a complete absence of clear entry and exit points.
Key Consequences
- Zero Modularity: Classes are deeply intertwined, making unit testing nearly impossible.
- High Error Proneness: Changing a small method in Class A can unexpectedly break functionality in Class D due to hidden side effects.
4. Anti-Pattern 3: Hard-Coding Values
Hard-coding involves embedding static, literal values directly inside constructors or business methods rather than injecting them via parameters, configuration files, or constants.
Why It's Dangerous
- Rigidity: If requirements change (e.g., changing a default connection string, timeout threshold, or static message), developers must hunt through source code to modify literals and recompile.
- Best Practice: Extract variable values into configuration files, environment variables, or dependency-injected constants.
5. Anti-Pattern 4: Gold Plating (Over-Engineering)
Gold Plating occurs when developers attempt to achieve absolute perfection by applying complex design patterns and abstractions to scenarios that are simple and unlikely to change.
The Over-Engineering Trap
- Designing an intricate factory, strategy, and command chain for a simple
if-elsestatement that will never have more than two static branches. - Rule of Thumb: Follow the mantra “Change what varies.” Do not introduce complexity for hypothetical futures that may never materialize.
6. Anti-Pattern 5: Breaking DRY (Do Not Repeat Yourself)
The DRY principle states that every piece of knowledge or business logic must have a single, unambiguous representation within a system.
The Anti-Pattern
- Copying and pasting identical blocks of validation or transformation logic across multiple methods (
M1,M2,M3). - The Fix: Extract duplicated code into utility classes, helper methods, or shared service layers so updates only need to be made in one place.
7. Anti-Pattern 6: Constructor Telescoping & Overloading
Creating an excessive number of overloaded constructors to handle various combinations of optional parameters.
Real-World Solution
- The Problem: It creates unreadable initialization code where developers must guess what each boolean or null parameter represents.
- The Solution: Use the Builder Design Pattern to construct complex objects step-by-step cleanly and readably.
8. Anti-Pattern 7: Overuse of Getters and Setters
Automatically generating getters and setters for every private member variable in a class via IDE shortcuts.
Why It's an Anti-Pattern
- Encapsulation Breakdown: Private variables exist to protect internal state. Exposing raw setters for every field turns objects into anemic data structures and bypasses domain validation rules.
- Best Practice: Only expose getters and setters when explicitly required by business rules, favoring immutable domain models where possible.
9. Anti-Pattern 8: Premature Optimization
Optimizing code for maximum performance or micro-benchmarking before ensuring correctness and readability.
Core Philosophy
"Make it work, then make it fast."
- Example in DSA: When solving a problem, first write a working brute-force approach (e.g., sorting) to guarantee correctness. Once verified, optimize it to using more advanced algorithms.
10. Anti-Pattern 9: Overuse of Inheritance
Relying heavily on deep, complex class inheritance hierarchies instead of composition.
Why It Fails
- Deep inheritance creates rigid, tightly coupled codebases where modifying a parent class breaks all subclasses (the Fragile Base Class problem).
- The Fix: Prefer object composition ("has-a" relationship) over class inheritance ("is-a" relationship).
Summary Table: Quick Reference of Design Anti-Patterns
11. Introduction to the Null Object Pattern
The Null Object Pattern is a behavioral design pattern that replaces the use of null references with an object that exhibits neutral ("do nothing") default behavior.
The Problem with null Checks
When working with object references, developers constantly write defensive checks to prevent NullPointerException:
if (object1 != null) {
object1.execute();
} else {
// Handle null case or risk application crash
}
Issues Caused by null:
- Cluttered Code: Codebases become littered with repetitive
if (obj == null)conditionals. - High Vulnerability: Forgetting a single
nullcheck leads to production crashes (NullPointerException). - Violation of Polymorphism: Instead of letting polymorphism handle execution seamlessly, developers write conditional branching based on the presence of
null.
12. Core Mental Model & Mechanics of the Null Object Pattern
Instead of returning null or passing null into method arguments, you provide a Null Object—a concrete implementation of an interface or abstract class that implements all methods as empty operations or default fallback values.
Step-by-Step Flow Comparison
Naive Approach (With null checks):
Null Object Pattern Approach:
13. Practical Code Architecture & Examples
Let's look at how the Null Object Pattern is applied in real-world design patterns covered throughout system design.
Example A: Strategy Design Pattern (Robot Movement)
Imagine a robot system where different robots have different movement capabilities. Some fly with wings, some with jets, and some do not fly at all.
Without Null Object:
// Robot must check if strategy is null before calling
if (flightStrategy != null) {
flightStrategy.fly();
} else {
// Do nothing
}
With Null Object (NoFlyBehavior):
public interface IFlyStrategy {
void fly();
}
public class JetFlyStrategy implements IFlyStrategy {
public void fly() {
System.out.println("Flying with high-speed jet!");
}
}
// Null Object Implementation
public class NoFlyBehavior implements IFlyStrategy {
public void fly() {
// Do nothing! Satisfies polymorphism safely without exceptions.
}
}
// Client usage: Never needs a null check
IFlyStrategy strategy = robot.getFlyStrategy();
strategy.fly(); // Safely executes even if the robot cannot fly!
Example B: Command Design Pattern (Remote Control)
In a home automation remote control, buttons can be assigned to various commands (e.g., LightOnCommand, ACMaxCommand). Unassigned buttons should not crash the remote.
public interface ICommand {
void execute();
}
// Null Object for unassigned buttons
public class NoCommand implements ICommand {
public void execute() {
// Intentionally left empty. Pressing an unassigned button does nothing safely.
}
}
public class RemoteControl {
private ICommand slot;
public RemoteControl() {
this.slot = new NoCommand(); // Default initialization instead of null
}
public void pressButton() {
slot.execute(); // Guaranteed never to throw NullPointerException
}
}
14. Summary and Key Takeaways
- Anti-Patterns Are Hidden Traps: Patterns like God Objects, Spaghetti Code, and Gold Plating degrade software architecture silently over time if left unchecked.
- Avoid God Objects: Keep orchestration classes simple and delegate operations downstream instead of centralizing all logic into one massive class.
- Follow DRY and KISS: Do not repeat yourself, and avoid over-engineering simple solutions (Gold Plating).
- Make It Work, Then Optimize: Prioritize getting software functional before engaging in premature optimization or micro-tuning.
- The Null Object Pattern Eliminates
null: Replace dangerousnullreferences with neutral, do-nothing concrete implementations of your interfaces. - Protect Against
NullPointerException: Using Null Objects ensures that client code can invoke methods polymorphically without defensiveif (obj == null)checks. - Proven Real-World Utility: The Null Object Pattern seamlessly integrates into behavioral patterns like Strategy (
NoFlyBehavior) and Command (NoCommand) to maintain clean, robust codebases.