Design Patterns #5: Null Object Pattern – Writing Safer, Cleaner Code.

Hey everyone, I’m back! Today, let’s talk about a common headache in programming: dealing with null objects. When you’re working with simple value types — like integers or booleans - you can rely on default values. An int defaults to 0, a bool to false, and so on. But what happens when you need to return an object? Many developers fall into the habit of returning null and then scattering null checks throughout their code. Sure, you could return a new instance instead of null, but that’s often just as problematic. So, what’s the better solution? Let’s dive in. Default Practice: Returning null for Missing Objects To illustrate the problem, let’s start with a basic example. Imagine we have a simple User class: public sealed class User(Guid id, string name, bool hasAccess) { public Guid Id { get; set; } = id; public string Name { get; set; } = name; public bool HasAccess { get; set; } = hasAccess; } Let's create a simple in-memory data store and implement a user lookup method. First, we'll seed some sample data: public sealed class NonNullObjectUserRepository { private readonly IList _users = new List(); public NonNullObjectUserRepository() { _users.Add(new User(Guid.Parse("8363c613-f614-4f7c-971f-396cba910f32"), "John", true)); _users.Add(new User(Guid.Parse("7c62b792-4903-4fe7-b264-870cdef66ea8"), "Jane", true)); _users.Add(new User(Guid.Parse("2e32e097-d729-4d12-838d-bcc4e6cd821a"), "Bob", true)); } public User? GetUserById(Guid id) { return _users.FirstOrDefault(u => u.Id == id); } } This is where we encounter our critical design decision - how should GetUserById behave when the requested user doesn't exist? The conventional approach would look something like this: public User? GetUserById(Guid id) { return _users.FirstOrDefault(u => u.Id == id); } Notice the return type User? - that question mark isn't just syntax. It's a warning sign that says: "This might be null" "You must handle this case" "I'm a potential runtime exception waiting to happen" Now let's create a service that acts as an intermediary between our application and the repository. This service will handle the business logic while delegating data access to the repository: public sealed class NonNullObjectUserService { private readonly NonNullObjectUserRepository _userRepository = new(); public User? GetCurrentUser(Guid userId) { return _userRepository.GetUserById(userId); } } Our GetCurrentUser method shares the same challenge - it returns a nullable User?, forcing callers to handle the null case. Next, let's create a method to make a call. For demonstration purposes, we'll attempt to retrieve a non-existent user using a random ID. Since the user may not exist, it's essential to perform a null check before using the object. While these checks can add to the codebase and increase complexity, they are necessary for safe access. Despite this, the code remains valid and functions as expected. public void RunGetNonNullObjectUser() { var userService = new NonNullObjectUserService(); var user = userService.GetCurrentUser(Guid.Parse("3d73ae77-229a-4f7b-a3c6-49a3e283b5ac")); if (user is null) { Console.WriteLine("User not found"); } } So, how can we make this better? This is where the Null Object Pattern comes in — it was specifically created to handle cases like this, where dealing with null checks can become repetitive and error-prone. A Practical Implementation of the Null Object Pattern Earlier, we created the User class. Now, we need to define an abstract type by introducing an IUser interface, which both User and other related classes will implement. The IUser interface should contain the same properties as the User class to ensure consistency and enable polymorphism. public interface IUser { public Guid Id { get; set; } public string Name { get; set; } public bool HasAccess { get; set; } } public sealed class User(Guid id, string name, bool hasAccess) : IUser { public Guid Id { get; set; } = id; public string Name { get; set; } = name; public bool HasAccess { get; set; } = hasAccess; } Next, let's implement a default class that provides properties with predefined values. We'll use the Singleton pattern here, as there's no need to create a new instance every time—the same instance can be reused throughout the application. public sealed class NullUser : IUser { public static readonly NullUser Instance = new (); private NullUser() { } public Guid Id { get; set; } = Guid.Empty; public string Name { get; set; } = string.Empty; public bool HasAccess { get; set; } = false; } The repository is similar to the previous one but now works with the general IUser type. Notice the GetNullObjectUserById method—here, we check if the retrieved o

Apr 21, 2025 - 16:11
 0
Design Patterns #5: Null Object Pattern – Writing Safer, Cleaner Code.

Hey everyone, I’m back! Today, let’s talk about a common headache in programming: dealing with null objects.

When you’re working with simple value types — like integers or booleans - you can rely on default values. An int defaults to 0, a bool to false, and so on. But what happens when you need to return an object?

Many developers fall into the habit of returning null and then scattering null checks throughout their code. Sure, you could return a new instance instead of null, but that’s often just as problematic.

So, what’s the better solution? Let’s dive in.

Default Practice: Returning null for Missing Objects

To illustrate the problem, let’s start with a basic example. Imagine we have a simple User class:

public sealed class User(Guid id, string name, bool hasAccess)
{
    public Guid Id { get; set; } = id;
    public string Name { get; set; } = name;
    public bool HasAccess { get; set; } = hasAccess;
}

Let's create a simple in-memory data store and implement a user lookup method. First, we'll seed some sample data:

public sealed class NonNullObjectUserRepository
{
    private readonly IList<User> _users = new List<User>();

    public NonNullObjectUserRepository()
    {
        _users.Add(new User(Guid.Parse("8363c613-f614-4f7c-971f-396cba910f32"), "John", true));
        _users.Add(new User(Guid.Parse("7c62b792-4903-4fe7-b264-870cdef66ea8"), "Jane", true));
        _users.Add(new User(Guid.Parse("2e32e097-d729-4d12-838d-bcc4e6cd821a"), "Bob", true));
    }

    public User? GetUserById(Guid id)
    {
        return _users.FirstOrDefault(u => u.Id == id);
    }
}

This is where we encounter our critical design decision - how should GetUserById behave when the requested user doesn't exist? The conventional approach would look something like this:

    public User? GetUserById(Guid id)
    {
        return _users.FirstOrDefault(u => u.Id == id);
    }

Notice the return type User? - that question mark isn't just syntax. It's a warning sign that says:

  • "This might be null"
  • "You must handle this case"
  • "I'm a potential runtime exception waiting to happen"

Now let's create a service that acts as an intermediary between our application and the repository. This service will handle the business logic while delegating data access to the repository:

public sealed class NonNullObjectUserService
{
    private readonly NonNullObjectUserRepository _userRepository = new();

    public User? GetCurrentUser(Guid userId)
    {
        return _userRepository.GetUserById(userId);
    }
}

Our GetCurrentUser method shares the same challenge - it returns a nullable User?, forcing callers to handle the null case.

Next, let's create a method to make a call. For demonstration purposes, we'll attempt to retrieve a non-existent user using a random ID. Since the user may not exist, it's essential to perform a null check before using the object. While these checks can add to the codebase and increase complexity, they are necessary for safe access. Despite this, the code remains valid and functions as expected.

    public void RunGetNonNullObjectUser()
    {
        var userService = new NonNullObjectUserService();
        var user = userService.GetCurrentUser(Guid.Parse("3d73ae77-229a-4f7b-a3c6-49a3e283b5ac"));
        if (user is null)
        {
            Console.WriteLine("User not found");
        }
    }

So, how can we make this better? This is where the Null Object Pattern comes in — it was specifically created to handle cases like this, where dealing with null checks can become repetitive and error-prone.

A Practical Implementation of the Null Object Pattern

Earlier, we created the User class. Now, we need to define an abstract type by introducing an IUser interface, which both User and other related classes will implement. The IUser interface should contain the same properties as the User class to ensure consistency and enable polymorphism.

public interface IUser
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public bool HasAccess { get; set; }
}

public sealed class User(Guid id, string name, bool hasAccess) : IUser
{
    public Guid Id { get; set; } = id;
    public string Name { get; set; } = name;
    public bool HasAccess { get; set; } = hasAccess;
}

Next, let's implement a default class that provides properties with predefined values. We'll use the Singleton pattern here, as there's no need to create a new instance every time—the same instance can be reused throughout the application.

public sealed class NullUser : IUser
{
    public static readonly NullUser Instance = new ();

    private NullUser() { }
    public Guid Id { get; set; } = Guid.Empty;
    public string Name { get; set; } = string.Empty;
    public bool HasAccess { get; set; } = false;
}

The repository is similar to the previous one but now works with the general IUser type. Notice the GetNullObjectUserById method—here, we check if the retrieved object is null, and if so, we return the default instance instead.

public sealed class NullObjectUserRepository
{
    private readonly IList<IUser> _users = new List<IUser>();

    public NullObjectUserRepository()
    {
        _users.Add(new User(Guid.Parse("8363c613-f614-4f7c-971f-396cba910f32"), "John", true));
        _users.Add(new User(Guid.Parse("7c62b792-4903-4fe7-b264-870cdef66ea8"), "Jane", true));
        _users.Add(new User(Guid.Parse("2e32e097-d729-4d12-838d-bcc4e6cd821a"), "Bob", true));
    }

    public IUser GetNullObjectUserById(Guid id)
    {
        var user = _users.FirstOrDefault(u => u.Id == id);
        return user ?? NullUser.Instance;
    }
}

The service remains the same as before, with the only difference being the return type, which is now IUser.

public sealed class NullObjectUserService
{
    private readonly NullObjectUserRepository _userRepository = new();

    public IUser GetCurrentNullObjectUser(Guid userId)
    {
        return _userRepository.GetNullObjectUserById(userId);
    }
}

Next, let's implement the call method. Unlike the previous version, this one includes a check for the default object.

    public void RunGetNullObjectUser()
    {
        var userService = new NullObjectUserService();
        var user = userService.GetCurrentNullObjectUser(Guid.Parse("3d73ae77-229a-4f7b-a3c6-49a3e283b5ac"));
        if (user.Id == Guid.Empty)
        {
            Console.WriteLine("User not found");
        }
    }

Comparison and Testing

As the codebase grows in complexity, we might question the need for the second approach—especially since it still involves checking the object. However, the key advantage lies in performance and code quality.

When checking for null, we're performing a reference check, which may be simple but often triggers compiler warnings or requires repetitive safeguards throughout the code. More null checks also increase branching, which can negatively impact performance and readability.

In contrast, the Null Object Pattern eliminates null references entirely. The object is never null, so we compare by value instead of by reference. This makes the code more predictable and improves branch prediction at runtime. It also results in cleaner Intermediate Language (IL) code with fewer instructions.

Although a direct null check is technically faster due to its simplicity, it often leads to more verbose and scattered null-handling logic. On the other hand, nullable objects typically produce more IL instructions. So, while the Null Object Pattern introduces a value-based check, it offers better maintainability, predictability, and, in some cases, performance benefits at scale.

benchmark 1

Certainly, we can modify our method to return a new instance instead of null. In this case, there's no need to check the object for null anymore.

    public User? GetUserById(Guid id)
    {
        var user = _users.FirstOrDefault(u => u.Id == id);
        return user ?? new User(Guid.Empty, string.Empty, false);
    }

    public void RunGetNonNullObjectUser()
    {
        var userService = new NonNullObjectUserService();
        var user = userService.GetCurrentUser(Guid.Parse("3d73ae77-229a-4f7b-a3c6-49a3e283b5ac"));
        if (user.Id == Guid.Empty)
        {
            Console.WriteLine("User not found");
        }
    }

However, this approach will be slower, as we create a new instance each time, which consumes more memory.

benchmark 2

Conclusions

In this article, I’ve aimed to clearly demonstrate the consequences of using null instead of the Null Object Pattern. While it's not critical for simple projects, in applications with complex logic and frequent reuse of services, performance may suffer.

I hope you found something valuable in this article. Happy coding, and see you next time!

You can find the source code for this example here.

Buy Me A Beer