Using NullObject pattern to avoid overfetching in DDD

Software Development Mar 17, 2021

You may have been in a situation like the following: you are building a REST API, and you want to follow a Domain-Driven Design approach.

There you go! You start writing a few classes, use cases, some repositories and implement some endpoints. You will realise sooner or later that if you have some relationships in your domain, you might need to fetch them all to build objects because, if you avoid one of them, you will end up in execution problems no matter what.

This post will show you how to deal with this problem using the NullObject pattern (with a bonus part using Singleton). However, before we start, I fully encourage you to read about DDD Aggregate Root since it is usually a better approach for dealing with this situation. If you are dealing with domain classes as you would in normal OOP, this is a possible solution in that situation.

Prerequisites

We do not need so much. We are using TypeScript with TypeORM in this example because I wanted to represent a real scenario, but you need a few simple domain classes and a repository. It is okay if you use an in-memory one.

As I said, this can be applied to any code architecture as long as you fetch data from a repository.

Our case

Imagine you have vehicles, and these vehicles, of course, are built using parts, and a manufacturer sold these parts. Great, we have identified our three domain classes for this example:

// Vehicle.ts
class Vehicle {
    private id: VehicleId
    private parts: VehiclePart[]
}

// VehiclePart.ts
class VehiclePart {
    private id: VehiclePartId
    private manufacturer: Manufacturer
}

// Manufacturer.ts
class Manufacturer {
    private id: ManufacturerId
}

So, at this point, you want to return a list of vehicles stored in your repository, so you have a repository and entity classes like these:

Note: This entity stuff comes from TypeORM. Check it out for more details.
// Vehicle.entity.ts
@Entity('vehicles')
class VehicleEntity extends BaseEntity {
    // ... Your fields and relations

    toDomainObject(): Vehicle {
        return new Vehicle({
            id: VehiclePartId.from(this.vehicleEntity.id),
            parts: this.vehicleEntity.parts.map(partEntity => partEntity.toDomainObject())
        })
    }
}

// VehicleRepository.ts
class VehicleRepository {
  private vehicleRepository = new Repository<VehicleEntity>() // VERY simplified

  async findAll(): Promise<Vehicle> {
      const vehicleEntities = await this.vehicleRepository.find()
      return vehicleEntities.map((vehicleEntity: VehicleEntity) => vehicleEntity.toDomainObject())
  }
}

We are skipping VehiclePart and Manufacturer entities and repositories, but they are very similar to Vehicle's implementation.

So, we have a VehicleEntity that implements a toDomainObject() method that takes our TypeORM entity and transforms it into a domain model. As you will be guessing, VehicleRepository's findAll() method does a SELECT query from the vehicles table and maps the result entities into domain vehicles.

Ok, so you call findAll(). Fine. Suddenly the program crashes with the following error:

TypeError: Cannot call method “toDomainObject” of undefined

What happened here? If nothing is specified, TypeORM will not JOIN VehicleEntity relations so, when we parse TypeORM vehicle entity objects into domain objects, vehicles’ parts entities are not present. To create a Vehicle object, we need to tell TypeORM we want vehicle parts in the findAll() method. Just add this to find the method:

async findAll(): Promise<Vehicle> {
      const vehicleEntities = await this.vehicleRepository.find({ relations: ['parts'] })
      return vehicleEntities.map(vehicleEntity => this.toDomainObject(vehicleEntity))
  }

Good job there! But what is this!?

TypeError: Cannot call method “toDomainObject” of undefined

Again!? Indeed, we forgot that we had another relation in VehiclePartEntity linking a Manufacturer, which shows up this error because you have to JOIN vehicle part manufacturer. Let's add another reference to our equation:

async findAll(): Promise<Vehicle> {
      const vehicleEntities = await this.vehicleRepository.find({ relations: ['parts', 'parts.manufacturer'] })
      return vehicleEntities.map(vehicleEntity => this.toDomainObject(vehicleEntity))
  }

Perfect, this now works, but imagine you have a lot of complex classes with several nested relations. You will have to JOIN every single relation, one by one, becoming a real pain in the ass. Not to mention the impact on the performance of doing this. Here is an example of how terrible your program can become:

// Note: This is just another way of joining relations in TypeORM
queryBuilder
          .leftJoinAndSelect('tasks.client', 'client')
          .leftJoinAndSelect('tasks.contact', 'contact')
          .leftJoinAndSelect('tasks.locations', 'locations')
          .leftJoinAndSelect('client.contact', 'client_contact')
          .leftJoinAndSelect('client.address', 'client_address')
          .leftJoinAndSelect('locations.contact', 'location_contact')
          .leftJoinAndSelect('locations.contract', 'location_contract')
          .leftJoinAndSelect('locations.site', 'location_site')
          .leftJoinAndSelect('locations.installer', 'location_installer')
          .leftJoinAndSelect('location_contract.checkpoints', 'checkpoints')
          .leftJoinAndSelect('checkpoints.previousCheckpoint', 'previousCheckpoint')
          .leftJoinAndSelect('location_installer.contact', 'installer_contact')
          .leftJoinAndSelect('location_installer.address', 'installer_address')
          .leftJoinAndSelect('location_site.address', 'site_address')

Are you laughing yet?

Let’s go back to our previous example so we can fix this situation. What will be the scenario if we want to return a list of vehicles with their parts without retrieving any manufacturer because we don’t need it in this particular case.
A potential solution would be to make these domain relationships optional, but you would end up with a lot of ugly conditionals that you DO NOT want in your code, like this example:

// Vehicle.ts
class Vehicle {
    private id: VehicleUuid
    private parts?: VehiclePart[]
    
    someMethodWithParts() {
      if (parts) { // <-- We want to avoid this
        // Do something
      }
    }
}

The approach I would like to suggest here is to use the NullObject pattern to choose which relations you want to join. If any relation is missing when calling to toDomainObject() method from Entity class, you pass a NullObject implementation. Just like this:

class NullVehiclePart extends VehiclePart {
    private id: VehiclePartUuid = VehicleUuid.from('')
    private manufacturer: Manufacturer = new NullManufacturer()

    someMethod() {
      // Nothing    
    }
}

With this class implementation, you can do this when building a domain object in your entity class:

@Entity('vehicles')
class VehicleEntity extends BaseEntity {
    // ... Your fields and relations

    toDomainObject(): Vehicle {
        return new Vehicle({
            id: VehiclePartUuid.from(vehicleEntity.id),
            parts: vehicleEntity.parts.map(partEntity => partEntity?.toDomainObject() || new NullVehiclePart())
        })
    }
}

With this, you can choose now which relations you want to join when using your repository methods, so you can return, as we wished to, vehicles with their parts only, optimising your queries and making your code simpler. This would be a simple approach:

  async findAll(relations: string[]): Promise<Vehicle> {
      const vehicleEntities = await this.vehicleRepository.find({ relations })
      return vehicleEntities.map(vehicleEntity => this.toDomainObject(vehicleEntity))
  }

But, hey! The post is not over yet.

Bonus track: using Singleton pattern to optimise our solution

Yes, this can get even better. Why do we have to create every single instance of NullVehiclePart when it always has the same value?
The Singleton pattern allows us to work with the same instance of a class during the program cycle, so we can reuse it in any place we want, without allocating more unnecessary memory, since all NullVehiclePart objects will be pointing to the same instance.

So now, let’s add one more class to our domain:

// NullVehiclePartSingleton.ts
import { NullVehiclePart } from './NullVehiclePart'

export class NullVehiclePartSingleton {
  private static instance: NullVehiclePart

  static getInstance() {
    if (!NullVehiclePartSingleton.instance) {
      NullVehiclePartSingleton.instance = new NullVehiclePart()
    }

    return NullVehiclePartSingleton.instance
  }
}

We have just declared a class with a static method that checks if a NullVehiclePart has been created before. If not, it will do so, and finally, it will return it. So, to use this, change VehicleEntity toDomainObject() method like this:

@Entity('vehicles')
class VehicleEntity extends BaseEntity {
    // ... Your fields and relations

    toDomainObject(): Vehicle {
        return new Vehicle({
            id: VehiclePartUuid.from(vehicleEntity.id),
            parts: vehicleEntity.parts.map(partEntity => 
              partEntity?.toDomainObject() ||
              NullVehiclePartSingleton.getInstance()
        })
    }
}

By doing so we will downsize the amount of memory allocated, since all NullObject declarations will be always the same instance, especially if we fetch large amounts of data and we do not want to fetch some relations when building some classes.

A real case of use: Wallbox

Wallbox is a company whose mission is to eliminate our dependency on fossil fuel by providing electric car charging solutions. In this context, Wallbox’s installation managers are one of the keys to succeeding in this operation. They are responsible for managing the electric supply installations from several locations in different countries, with different settings, requirements, possibilities, etc. Here is where we take part in the action. Our goal is to digitalize these installation processes to handle these tasks in less time and one single platform, our SPA, built with ReactJS with NextJS.

To feed this platform, our backend team has been building a REST API with the NestJS framework with TypeORM, of course. Who else could be the player taking into account the post’s title? The point is that, after a couple of months, we were experiencing the same problems I have just explained at the beginning of this post. In the Wallbox Installation Manager platform, the most important entity is the Task. A task is an installation request from a client, which can have several locations to place a charger. These locations have a physical place to install the charger, named site. And these sites can have several products configured in a certain way, with some specific rates that are only applied in a particular country, etc. Well, as you might have noticed, this Task domain class has so many relationships.

Our problem was pretty simple, indeed. To fetch a single task, we had to JOIN several tables to build a Task domain object, whereas we were using its attributes and, maybe, some of the nested relations. So, in the end, our task repository was looking like this in every finder method:

        queryBuilder
          .leftJoinAndSelect('tasks.locations', 'locations')
          .leftJoinAndSelect('locations.contract', 'locations_contract')
          .leftJoinAndSelect('locations.contact', 'locations_contact')
          .leftJoinAndSelect('locations.installer', 'locations_installer')
          .leftJoinAndSelect('locations_installer.address', 'locations_installer_address')
          .leftJoinAndSelect('tasks.client', 'client')
          .leftJoinAndSelect('client.contact', 'client_contact')
          .leftJoinAndSelect('client.address', 'client_address')
          .leftJoinAndSelect('tasks.contact', 'contact')
          .leftJoinAndSelect('contact.country', 'contact_country')
          .leftJoinAndSelect('contact.contact', 'contact_contact')
          .leftJoinAndSelect('tasks.installer', 'installer')

We did not even bother about the locations’ installers’ addresses for showing a list of tasks. Still, we had to JOIN them to build a location domain object without getting execution errors. So, how did we stop this unnecessary and tedious behaviour?

Our first approach was to make all relations optional, so we put a ? in every domain class fields definition:

interface TaskAttributes {
    id?: number
    uuid: string
    identifier: string
    state: TaskState
    notes?: string
    createdAt?: Date
    updatedAt?: Date
    deletedAt?: Date
    client: Client
    locations?: Location[]
    contact?: Contact
    activityLog?: ActivityLog
    admin?: Admin
}

The main issue this approach lead comes when a method involving any of these “optional” relations comes in, like in this example of the task’s calculateTotalBasePrice() method:

  calculateTotalBasePrice(): number {
    if (!this.locations) return 0

    return this.locations.reduce(
      (accumulatedTotalPrice, location) => accumulatedTotalPrice + location.calculateBasePrice(),
      0
    )
  }

A task has an array of locations. To calculate the tasks’ base price, we need to loop every location and estimate its cost. If our locations are an empty array, reduce will return 0, but if locations can be undefined, we need to check if they are not first to behave accordingly. And this turns out to be slightly annoying when you have to do so for every relation your domain class has.

NullObject pattern solved this particular problem. As the previous example, we just created a NullLocationSingleton class, and we returned it in TaskEntity’s toDomainObject() method if locations were not joined within a task. And we applied this to every relation:

class TaskEntity extends BaseEntity {
  // Some stuff...
  
  toDomainObject(): Task {
    return new Task({
      id: this.id,
      uuid: this.uuid,
      identifier: this.identifier,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      client: this.client?.toDomainObject() || NullClientSingleton.getInstance(),
      contact: this.contact?.toDomainObject() || NullContactSingleton.getInstance(),
      documents: this.documentFiles?.map(documentFile => documentFile.toDomainObject()),
      activityLog: this.activityLog?.toDomainObject(),
      locations: this.locations?.map(location => location.toDomainObject()) || [],
      admin: this.admin?.toDomainObject(),
    })
  }
}

Thanks to this approach, we can JOIN the relations we need in the different use cases we have. As you may have seen, it is unnecessary to apply NullObject pattern to arrays nor optional relationships

Some conclusions on this matter

To conclude, I would say that this technique is beneficial when we have a massive quantity of classes with lots of relationships between them, but you do not need to load them always. Besides, it may not be worth the time if you have a little bunch of relationships in the class, or your project is not that complex yet.

I hope you found this post helpful. Anyway, as I previously said, I encourage you to investigate other methods or approaches and make sure you share your thoughts in the comments so we can keep learning; this is one more of them that may or may not fit with your problem. I am very excited to know about other people concerns.

Before we end, I would like to thank my colleagues Juan Nogueras and Dani Ramos, who gave me the main concepts to build this idea.

See you guys in a new post soon!

Jaume Moreno Cantó

King of Back-End Development