📖 What is the Null Object Pattern?

The Null Object Pattern is a behavioral design pattern where:

  • Instead of returning null (which can cause errors if used without checking),
  • You return a special object that does nothing but still behaves like a real object.

✅ This avoids if (obj != null) checks everywhere in your code
✅ It helps follow Open/Closed Principle (extend behavior without changing logic)


🎨 Real-World Examples (Simple to Visualize)

👤 1. Unknown User (Guest)

  • Instead of: Returning null for unauthenticated users
  • Do this: Return a GuestUser object that has safe defaults (no login, read-only)

🧃 2. Empty Cart

  • Instead of: if (cart) { cart.checkout() }
  • Do this: Use EmptyCart object with .checkout() method that logs "No items"

🗃️ 3. Logger

  • A ConsoleLogger logs to the console
  • A NullLogger just does nothing (used in production or testing)

🧠 Why Use It?

✅ Avoid runtime errors from null
✅ Reduce code clutter from if (obj !== null)
✅ Improve readability & testability
✅ Follow Polymorphism instead of conditionals


🧱 TypeScript Example — Logger Pattern

Let's build a real use case: a logger.


1. Define Logger Interface

interface Logger {
  log(message: string): void;
}

2. Real Logger (Console)

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}

3. Null Logger (does nothing)

class NullLogger implements Logger {
  log(message: string): void {
    // do nothing
  }
}

4. Service Using Logger

class UserService {
  constructor(private logger: Logger) {}

  createUser(name: string) {
    // ... create logic
    this.logger.log(`User created: ${name}`);
  }
}

5. Use It Without Null Check

const consoleLogger = new ConsoleLogger();
const nullLogger = new NullLogger();

const service1 = new UserService(consoleLogger);
service1.createUser("Alice");  // logs: User created: Alice

const service2 = new UserService(nullLogger);
service2.createUser("Bob");    // logs nothing, no error

✅ No need to check if (logger != null) — just inject the correct behavior.


💡 Better Than Null Check

Instead of:

if (logger) {
  logger.log("something");
}

With Null Object:

logger.log("something"); // always safe!

🔄 Real-World Use Cases

Context Null Object
Logging NullLogger (test env, prod)
Payment NullPaymentProcessor (free plan)
User GuestUser (not signed in)
Cart EmptyCart (user hasn't added anything)
Strategy NoOpStrategy for default behavior

🎯 Pro Tips for Mid-to-Senior Devs

✅ Replace null/undefined returns with Null Object classes
✅ Let Null Object implement the same interface
✅ Makes code cleaner, testable, and follows polymorphic behavior


🧪 Testability Bonus

In unit tests, instead of mocking a logger:

const service = new UserService(new NullLogger());

No mocks needed — just use the Null Object!


🔚 Final Summary

"The Null Object Pattern replaces null with an object that safely does nothing — reducing checks and avoiding errors."


Perfect! Let’s now apply the Null Object Pattern in a NestJS service layer — a common place where it adds a lot of clarity and safety. I’ll walk you through a practical example slowly and clearly.


✅ Use Case: Optional Logger in a NestJS Service

Let’s say you have a UserService that logs events (like user creation), but sometimes you don’t want logging (e.g., during tests or in certain environments). You don’t want to check if (logger) everywhere.


🧱 Step-by-Step: Null Object Pattern in NestJS


🔹 1. Create a Logger Interface

// logger/logger.interface.ts
export interface LoggerService {
  log(message: string): void;
}

🔹 2. Real Logger Implementation

// logger/console-logger.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.interface';

@Injectable()
export class ConsoleLoggerService implements LoggerService {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

🔹 3. Null Logger (does nothing)

// logger/null-logger.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.interface';

@Injectable()
export class NullLoggerService implements LoggerService {
  log(message: string): void {
    // do nothing
  }
}

🔹 4. Inject Logger into Your Service

// user/user.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from '../logger/logger.interface';

@Injectable()
export class UserService {
  constructor(private readonly logger: LoggerService) {}

  createUser(username: string) {
    // ... your real user creation logic
    this.logger.log(`User created: ${username}`);
  }
}

🔹 5. Provide Either Logger in Your Module

// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user/user.service';
import { ConsoleLoggerService } from './logger/console-logger.service';
import { NullLoggerService } from './logger/null-logger.service';
import { LoggerService } from './logger/logger.interface';

@Module({
  providers: [
    UserService,
    {
      provide: LoggerService,
      useClass:
        process.env.NODE_ENV === 'test' ? NullLoggerService : ConsoleLoggerService,
    },
  ],
})
export class AppModule {}

✅ Now:

  • In test env, it uses NullLoggerService (no logs, no noise)
  • In prod/dev, it uses ConsoleLoggerService (full logs)

🎯 Why This Works Well

Benefit How It Helps
✅ Avoid null checks No if (logger) needed anywhere
✅ Clean DI Swap behaviors easily at runtime
✅ Safe by default NullLogger never throws
✅ Open/Closed Principle Add new loggers without changing service logic
✅ Testing-friendly Inject NullLoggerService in tests, no mocking needed

🧪 Test Usage (Easy)

const module = await Test.createTestingModule({
  providers: [
    UserService,
    { provide: LoggerService, useClass: NullLoggerService },
  ],
}).compile();

const service = module.get(UserService);
service.createUser('Alice'); // runs silently

🧠 You Can Extend This Pattern To:

  • AnalyticsServiceNullAnalyticsService
  • EmailServiceNullEmailService
  • CacheServiceNullCacheService for dev mode
  • NotificationServiceNullNotificationService

🧵 Final Summary

In NestJS, use the Null Object Pattern by creating default "no-op" services that follow the same interface as real ones — this simplifies logic, removes conditionals, and improves testing and runtime flexibility.