Clean&Reactoring Clean Code Created: 09 Feb 2026 Updated: 09 Feb 2026

Null Object Pattern: Cleaner Business Logic in C#

In business applications, we often deal with "optional" rules. For example, a customer might have a discount code, or they might have a loyalty membership.

A common mistake is treating the absence of a discount as a null value. This forces your pricing engine to constantly check if (discount != null) before calculating the total.

The Null Object Pattern solves this by treating "No Discount" as a valid, existing strategy that simply does nothing (returns the price unchanged).

The Scenario: Pricing Engine

We are building a checkout system. We have an interface IDiscountStrategy that calculates the final price.

❌ The "Bad" Example: Pollution with Null Checks

In this approach, if a customer is a "Guest" (no account), the DiscountStrategy property is null.

public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal orderTotal);
}

public class PercentageDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal orderTotal)
{
return orderTotal * 0.90m; // 10% off
}
}

public class OrderProcessor
{
private readonly IDiscountStrategy _discount;

// The discount is optional, so we allow nulls
public OrderProcessor(IDiscountStrategy discount)
{
_discount = discount;
}

public decimal CalculateFinal(decimal price)
{
// ❌ BAD: We must branch our logic.
// If we forget this check, the system crashes.
if (_discount != null)
{
return _discount.ApplyDiscount(price);
}
else
{
// Fallback behavior hard-coded here
return price;
}
}
}

Why this is bad for business logic:

  1. Inconsistent Rules: The definition of "what happens when there is no discount" is hidden inside the if/else block of the OrderProcessor.
  2. Scalability: If you add tax calculation, shipping rules, and referral bonuses, your CalculateFinal method will become a mess of nested if (!= null) checks.

✅ The "Good" Example: The Null Object Strategy

Instead of passing null, we create a NoDiscount class. It implements the interface but applies neutral behavior (it returns the price exactly as it is).

// 1. The Null Object
// This represents "No Discount", but it is a real object.
public class NoDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal orderTotal)
{
// Neutral behavior: The price does not change.
return orderTotal;
}
}

public class OrderProcessor
{
private readonly IDiscountStrategy _discount;

public OrderProcessor(IDiscountStrategy discount)
{
// ✅ GOOD: We ensure a strategy always exists.
// If the caller passes null, we assign the NoDiscount strategy.
_discount = discount ?? new NoDiscount();
}

public decimal CalculateFinal(decimal price)
{
// ✅ CLEAN: The business logic is linear.
// We don't care if it's a VIP discount or No Discount.
// We just ask the strategy to do its job.
return _discount.ApplyDiscount(price);
}
}

Usage in the Real World

Now, your checkout flow is uniform for all customers.

// Scenario A: VIP Customer
var vipOrder = new OrderProcessor(new PercentageDiscount());
decimal vipPrice = vipOrder.CalculateFinal(100); // Returns 90

// Scenario B: Guest Customer
// We can explicitly pass the "Null Object" (NoDiscount)
// OR pass null (handled by constructor)
var guestOrder = new OrderProcessor(new NoDiscount());
decimal guestPrice = guestOrder.CalculateFinal(100); // Returns 100

Summary

FeatureNull Checks (Bad)Null Object Pattern (Good)
Logic"If discount exists, apply it. Else, return price.""Apply the discount."
ResponsibilityThe Processor decides the default behavior.The NoDiscount class owns the default behavior.
Crash RiskHigh (NullReferenceException)None (Object always exists)
MaintenanceMessy if/else chains.clean, polymorphic method calls.

By using the Null Object Pattern, you remove the concept of "missing data" from your business logic. A customer with no discount isn't a "null case"; they are simply a customer with a "0% off strategy."

Share this lesson: