CacheU
Low Level Design

Anti-Patterns and Null Object Pattern

A detailed guide to common anti-patterns in software design and the Null Object pattern, including spaghetti code, God object, hard coding, DRY violation, gold plating, null checks, UML diagrams, and implementations in C++, Java, and Python.

Anti-Patterns and the Null Object Pattern

Software design is not only about choosing the right patterns.
It is also about recognizing the wrong habits that slowly make code harder to understand, harder to test, and harder to maintain.

These harmful habits are often called anti-patterns.

At the same time, one common source of messy code is repeated null checks.
The Null Object Pattern offers a clean way to avoid them by using polymorphism instead of conditionals.

This document covers both:

  • common anti-patterns
  • the Null Object Pattern as a better alternative to repetitive null handling

Introduction: What is an Anti-Pattern?

An anti-pattern is a commonly used solution that seems helpful in the short term, but usually creates bigger problems later.

It is not just “bad code.”

It is code or design that:

  • looks acceptable at first
  • solves the immediate issue
  • creates maintainability, readability, or scalability problems later

Why anti-patterns matter

Anti-patterns often lead to:

  • tight coupling
  • duplicated logic
  • fragile code
  • poor maintainability
  • difficult debugging
  • high testing effort

The most dangerous part is that they often appear gradually.

A developer may not notice the damage until the codebase has already grown large.


Common Categories of Anti-Patterns

Anti-patterns often fall into several categories:

CategoryMeaning
OrganizationalProblems caused by bad team/process decisions
DevelopmentalProblems caused by coding habits or implementation choices
ArchitecturalProblems caused by poor structure and design

This guide focuses on developmental and architectural issues.


Overview of the Anti-Patterns Covered

Anti-PatternProblem
Spaghetti CodeLogic is tangled and hard to follow
God ObjectOne class does too much
Hard CodingValues are fixed directly in code
Violating DRYThe same logic is repeated everywhere
Gold PlatingOver-engineering beyond actual needs

1. Spaghetti Code

Spaghetti Code is code that is tangled, confusing, and difficult to understand.

It usually has:

  • unclear structure
  • too many dependencies
  • nested conditionals
  • poor separation of concerns
  • hidden side effects

It is called “spaghetti” because the logic looks like a bowl of tangled noodles.


Why it is dangerous

When code becomes spaghetti code:

  • one change can break many unrelated parts
  • debugging becomes painful
  • adding features becomes risky
  • new developers struggle to understand the flow

Example of the problem

Imagine validation logic, login logic, notification logic, and billing logic all mixed together in the same method.

That makes the code extremely hard to manage.


Spaghetti Code diagram

Diagram
flowchart TD A[Main Function] --> B[Validation] B --> C[Login] C --> D[Notifications] D --> E[Billing] E --> F[Logging] F --> B

The arrows loop and cross each other, showing tangled control flow.


How to avoid it

Good PracticeBenefit
Use small functionsEasier to read
Use clear class boundariesBetter organization
Separate concernsLess coupling
Use meaningful namesEasier maintenance
Refactor earlyPrevents long-term mess

2. God Object

A God Object is a single class that knows too much and does too much.

It tries to:

  • store data
  • process business logic
  • manage UI
  • handle network calls
  • send emails
  • log actions
  • coordinate everything

This class becomes a central “super object” holding too much power.


Why it is dangerous

A God Object creates:

  • high coupling
  • low modularity
  • difficult testing
  • weak maintainability
  • single point of failure

If this class breaks, many parts of the system may fail.


Example

A UserManager class that:

  • stores user profile
  • processes payments
  • sends emails
  • logs actions
  • manages authentication

This is a classic God Object smell.


God Object diagram

Diagram
flowchart TD A[God Object] --> B[User Profiles] A --> C[Payments] A --> D[Emails] A --> E[Logging] A --> F[Authentication] A --> G[Notifications]

One object is acting like a giant central hub.


How to avoid it

Good PracticeBenefit
Apply SRPOne class, one reason to change
Break big classesEasier to manage
Use service layersClear responsibilities
Separate domain logicBetter testability
Use compositionLess coupling

3. Hard Coding

Hard coding means putting fixed values directly into source code.

Examples:

  • file paths
  • usernames
  • tax rates
  • port numbers
  • greeting messages
  • magic constants

Why it is dangerous

Hard coding makes software:

  • hard to change
  • hard to reuse
  • hard to configure
  • brittle in production

If a value changes, you may need to update many places.


Example

Imagine the tax rate is written as 0.07 in many files.

If the tax changes to 0.075, every occurrence must be updated carefully.

If even one place is missed, the system becomes inconsistent.


Hard Coding diagram

Diagram
flowchart LR A[Source Code] --> B[Fixed Value 1] A --> C[Fixed Value 2] A --> D[Fixed Value 3] A --> E[Fixed Value 4]

The same value is scattered in many places.


How to avoid it

Good PracticeBenefit
Use constantsCentralized values
Use config filesEasy environment changes
Pass values as parametersFlexible behavior
Use environment variablesDeployment friendly
Use dependency injectionMore testable code

4. Violating DRY

DRY stands for Don’t Repeat Yourself.

This anti-pattern happens when the same logic is copied into multiple places.


Why it is dangerous

When logic is duplicated:

  • updates become expensive
  • bugs are repeated
  • behavior becomes inconsistent
  • maintenance becomes harder

If one copy is changed and another is forgotten, the system behaves differently in different places.


Example

Suppose the same password validation logic is copied into:

  • sign up
  • login
  • change password

Now every password policy change requires editing three locations.


DRY violation diagram

Diagram
flowchart TD A[Password Validation Logic] --> B[Sign Up] A --> C[Login] A --> D[Change Password]

The same logic is duplicated in multiple places.


How to avoid it

Good PracticeBenefit
Extract functionsReuse logic
Create helper classesCleaner design
Use shared utilitiesSingle source of truth
Refactor duplication earlyPrevents growth of repetition

5. Gold Plating

Gold plating is the habit of adding unnecessary complexity to a solution.

It usually comes from:

  • trying to make everything perfect
  • over-engineering
  • planning for imaginary future problems
  • adding features nobody asked for

Why it is dangerous

Gold plating wastes:

  • time
  • effort
  • testing cost
  • maintenance effort

The result is often a huge system that solves a tiny problem.


Example

Instead of a simple username save function, a developer builds:

  • internationalization
  • history tracking
  • profanity filtering
  • audit logging
  • rollback support

for a feature that only needs a simple internal admin edit.

That is too much.


Gold plating diagram

Diagram
flowchart TD A[Simple Problem] --> B[Overly Complex Solution] B --> C[Extra Features] B --> D[Extra Classes] B --> E[Extra Testing] B --> F[Extra Maintenance]

How to avoid it

Good PracticeBenefit
Solve the actual problemKeeps focus
Start simpleLess waste
Add features only when neededBetter agility
Prefer practical designLower maintenance cost
Refactor later if necessaryControlled evolution

Anti-Patterns Summary Table

Anti-PatternCore ProblemMain Risk
Spaghetti CodeTangled structureHard to understand and change
God ObjectToo many responsibilitiesSingle point of failure
Hard CodingFixed values in sourcePoor flexibility
Violating DRYDuplicate logicInconsistent updates
Gold PlatingUnnecessary complexityOver-engineering

Why anti-patterns usually happen

Anti-patterns often appear because of:

  • short-term fixes
  • rushing deadlines
  • lack of architectural thinking
  • copying existing mistakes
  • failure to refactor early

They are often not malicious. They are usually the result of pressure and habit.


How to recognize anti-patterns early

Look for these warning signs:

Warning SignPossible Problem
Very large classGod Object
Long nested conditionsSpaghetti Code
Same logic in many filesDRY violation
Fixed values repeatedHard coding
Complex solution for a tiny taskGold plating

How to avoid anti-patterns

PrincipleWhat it helps with
SRPPrevents God Objects
DRYPrevents duplication
OCPSupports extension without rewriting
Composition over inheritanceReduces tangled designs
Configuration over hard codingImproves flexibility
Refactoring regularlyStops code decay

Pro Tip: The Null Object Pattern

A very common coding annoyance is repetitive null checks.

For example:

if (obj != null) {
    obj.doSomething();
}

This appears everywhere in many codebases.

The Null Object Pattern removes the need for such checks.


What problem does it solve?

Sometimes a method may return null to indicate “nothing here.”

That forces the client to write:

  • null checks
  • fallback logic
  • special cases

If one null check is forgotten, the program may crash.


Why null is troublesome

Calling a method on null causes errors such as:

  • NullPointerException in Java
  • segmentation issues in lower-level languages
  • runtime crashes in general

The Null Object Pattern Idea

Instead of returning null, return a special object that:

  • implements the same interface
  • does nothing
  • returns sensible default values

This lets the client treat real and null objects the same way.


Null Object Pattern concept

Diagram
flowchart TD A[Client Request] --> B{Real Object Available?} B -->|Yes| C[Return Real Object] B -->|No| D[Return Null Object] D --> E[Safe default behavior]

Why this is elegant

The client code no longer needs to check:

  • is object null?
  • should I do fallback?
  • should I skip this operation?

The object itself handles the “do nothing” behavior.


Null Object Pattern benefits

BenefitDescription
Removes null checksCleaner client code
Prevents crashesLess chance of null reference errors
Uses polymorphismReal object and null object share same interface
Keeps client simpleNo special-case code everywhere
Follows LSPNull object can replace real object safely

Important design idea

The Null Object Pattern follows the principle:

Replace conditionals with polymorphism.

Instead of asking:

  • “Is this object null?”

We simply call the method and let the object handle it.


Null Object in real patterns

Null Object is often used as:

  • NoFlyBehavior
  • NoCommand
  • NoDiscount
  • EmptyLogger
  • NullCustomer
  • NullPaymentProcessor

Example: Strategy Pattern with NoFlyBehavior

A robot may use:

  • FlyWithWings
  • FlyWithJets
  • NoFlyBehavior

Instead of checking for null, the robot always has a fly behavior.

If it cannot fly, the behavior simply does nothing.


Example: Command Pattern with NoCommand

A button may have:

  • a real command
  • or a NoCommand

Instead of checking whether the button is assigned, the system always calls execute().

If there is no command, NoCommand.execute() simply does nothing.


Null Object vs null reference

Feature`null` referenceNull Object
Needs null checksYesNo
Safe to call methodsNoYes
Follows polymorphismNoYes
Cleaner client codeNoYes

Example

#include <iostream>
#include <memory>
using namespace std;
 
class Logger {
public:
    virtual void log(const string& message) = 0;
    virtual ~Logger() = default;
};
 
class ConsoleLogger : public Logger {
public:
    void log(const string& message) override {
        cout << "LOG: " << message << endl;
    }
};
 
class NullLogger : public Logger {
public:
    void log(const string& message) override {
    }
};
 
class PaymentService {
private:
    shared_ptr<Logger> logger;
 
public:
    PaymentService(shared_ptr<Logger> logger) : logger(logger) {}
 
    void makePayment(int amount) {
        logger->log("Starting payment");
        cout << "Processing payment of " << amount << endl;
        logger->log("Payment completed");
    }
};
 
int main() {
    shared_ptr<Logger> realLogger = make_shared<ConsoleLogger>();
    shared_ptr<Logger> nullLogger = make_shared<NullLogger>();
 
    PaymentService service1(realLogger);
    PaymentService service2(nullLogger);
 
    service1.makePayment(500);
    service2.makePayment(1000);
 
    return 0;
}

C++ explanation

  • Logger is the common interface
  • ConsoleLogger is the real object
  • NullLogger is the null object
  • client code calls log() without checking for null
  • the null logger safely does nothing

Java explanation

  • NullLogger implements the same interface
  • it safely performs no action
  • no null checks are needed in PaymentService
  • client code is simpler and cleaner

Python explanation

  • NullLogger implements the same interface
  • method does nothing
  • client code remains simple
  • no if logger is not None checks are needed

Null Object in UML

Diagram
classDiagram class Logger { +log } class ConsoleLogger { +log } class NullLogger { +log } class PaymentService { -logger +makePayment } Logger <|.. ConsoleLogger Logger <|.. NullLogger PaymentService --> Logger

How Null Object helps with anti-patterns

Null Object can reduce:

  • repetitive null checks
  • messy conditional logic
  • error-prone fallback code

This improves readability and lowers the risk of runtime errors.


Anti-Patterns vs Good Practices

Bad HabitBetter Alternative
Spaghetti CodeSmall focused modules
God ObjectSeparate responsibilities
Hard CodingConfig/constants/parameters
DRY violationExtract reusable logic
Gold PlatingBuild only what is needed
Repeated null checksNull Object Pattern

Practical refactoring advice

When you notice one of these anti-patterns:

  1. identify the repeated smell
  2. isolate the responsibility
  3. extract a smaller unit
  4. remove duplication
  5. simplify the design
  6. test the refactor carefully

Summary

Anti-patterns are common bad habits that create long-term pain in codebases.

The major ones covered here are:

  • Spaghetti Code
  • God Object
  • Hard Coding
  • Violating DRY
  • Gold Plating

A major source of messy code is repetitive null handling. The Null Object Pattern solves that elegantly by replacing null checks with polymorphism.


Final takeaway

Good software design is often about avoiding the wrong habits before they grow.

Remember:

A quick shortcut today can become a maintenance nightmare tomorrow.

Watch for anti-patterns early, refactor regularly, and use patterns like the Null Object Pattern to keep your code clean, safe, and easy to evolve.