Repository pattern explained with Laravel and NestJS examples

The repository pattern is one of the simplest patterns to implement in terms of code and abstraction complexity, but it is also one of the most misrepresented and misused concepts in real-world scenarios and applications. I’ll try to express my thoughts on this.

The concept

Let’s start with the repository concept. How it appears and how to carry out a specific code implementation. The repository should be located between the business logic domain and the data mapping layer, and it should provide an interface for accessing domain objects. Repository encapsulates the data from whatever data storage system you use (database, file, some caching system, external API, etc.) and the operations performed on it, allowing for more object-oriented data layer management. The repository also helps to achieve a clean separation of concerns between the business logic and data mapping layers.

To avoid using pseudo code, the following examples will be written in PHP using Laravel-specific syntax, but I promise to keep them simple enough that you can understand them even if you have no experience with PHP or Laravel. 🙂

When to use them and when not to use them

But first, let’s dispel some myths about when repositories are most useful for implementation and when they’re probably overkill for your code architecture. When we use a modern web development framework, such as Laravel, Symfony, NestJS, or any other multipurpose full stack framework, we usually add or rely on some specific ORM tools. Laravel has Eloquent, Symfony has Doctrine, and NestJS has options such as TypeORM, MikroORM, and Mongoose, so basically all of these data mappers are tools that actually do the above-mentioned abstraction, a layer that lays and is used between the business logic implementation (service, action, interactor classes, or whatever design you have accepted) and the data layer. So the debate will be about why we need repositories in the first place and in which cases they will be useful.

Implementing the repository pattern in your application is probably overkill in the following scenarios:

  • Using it in simple cases, such as retrieving data from a single table with a single model
  • Using it in slightly more complex cases that could be solved with simple model relation queries or something similar

Most web application services require the use of a simple database query that performs some simple database operations. This pattern is most likely an over-engineering solution in such cases. This does not preclude you from using Model-based queries in your domain logic. This can still be abstracted within simple Query classes that can be called directly without the extra complexity of creating interfaces and custom return objects like DAO classes or similar. You can still separate these Query classes into different namespaces and call them Eloquent Query classes or something similar to indicate that they are returning Eloquent collections or Model objects. This may be useful if you still want to separate your data access logic from your domain logic, which is usually fine.

Using repositories may be advantageous when:

  • Your application aggregates and operates on a complex dataset through the use of multiple databases, and it functions as a multi-tenant service. If you are unsure how your application will switch common functionalities between databases in the future, repositories may be useful. Because repositories are centralized and implemented in the business logic domain via interfaces, they are easily switched.
  • Your application aggregates data from external APIs or Elasticsearch, and you need to abstract the data management layer because you won’t have the capabilities of the ORM tool in such cases.
  • You have complex queries in your code that you need to call from different service classes.
  • Custom actions on an entity model must be implemented to perform data manipulation statements when moving data from/to the database.
  • Implementing caching systems on top of your standard database connection.

Laravel usage examples

Let’s look at how we can do this with the Laravel framework. To begin, in order to properly implement the repository pattern, you must first create a contract that the specific repository class must adhere to. Assume we have some complex database queries that need to be run over your e-commerce products, and the interface should look like this:

interface ProductRepositoryInterface
{
    public function setAsOutOfStock(int $id): OutOfStockDAO;
    public function setAsAvailable(int $id): AvailableDAO;
    public function getDistributors(int $id): DistributorDAO;
    public function getPrices(int $id): PriceDAO;
    public function addDiscount(int $id, array $discounts): DiscountDAO;
}

The actual implementation should now look like this:

class ProductRepository implements ProductRepositoryInterface
{
    public function setAsOutOfStock(int $id): OutOfStockDAO
    {
        /**
         * You'll probably have a complex query here using Laravel
         * custom database builder or something similar
         */

         DB::table('products')->select(...)...;
    }

    public function setAsAvailable(int $id): AvailableDAO
    {
        /**
         * Or using a raw query
         */

         DB::raw(...)
    }

    ...
}

You can see where this is going, and you’re probably guessing what comes next. The implementation class and the interface must now be bound so that the Laravel service container knows how to resolve their dependency. You must do this in the AppServiceProvider or create a custom service provider.

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->bind(
            ProductRepositoryInterface::class,
            ProductRepository::class
        );
    }
}

You can now use the repository in your controllers, action classes, and service classes as follows:

class ProductService
{
    public function __construct(
        public ProductRepositoryInterface $productRepository
    ) {}

    public function setAsOutOfStock(int $id): bool
    {
        $response = $this->productRepository->setAsOutOfStock($id);
        ...
    }

    public function setAsAvailable(int $id): bool
    {
        $response = $this->productRepository->setAsAvailable($id);
        ...
    }

}

NestJS usage examples

If you’ve read my blog before, you may have noticed that I like the Node.js framework NestJS. If you have some experience with Node.js or want to try Node.js and haven’t heard of or tried this framework, I strongly advise you to do so. It is a truly progressive framework with many features out of the box, excellent support and community, enterprise readiness, and a beautiful design that includes full Typescript support.

If we want to do something similar with NestJS and the TypeORM, we must first define the Product entity.

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column({ default: true })
  isActive: boolean;

  ...
}

The Product entity file should be placed in the Products directory. This directory contains all ProductsModule-related files. You can keep your model files wherever you want, but it’s best to keep them near their domain, in the corresponding module directory. Let’s make a Repository provider now:

import { DataSource } from 'typeorm';
import { Product } from './product.entity';

export const productProviders = [
  {
    provide: 'PRODUCT_REPOSITORY',
    useFactory: (dataSource: DataSource)
      => dataSource.getRepository(Product),
    inject: ['DATA_SOURCE'],
  },
];

Just keep in mind that magic strings should be avoided. PRODUCT REPOSITORY and DATA SOURCE should be kept separate in the constants.ts file.

Using the @Inject() decorator, we can now inject the Repository into the ProductService:

import { Injectable, Inject } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Product } from './product.entity';

@Injectable()
export class ProductService {
  constructor(
    @Inject('PRODUCT_REPOSITORY')
    private productRepository: Repository<Product>,
  ) {}

  async setAsOutOfStock(): Promise<Product[]> {
    let response = this.productRepository...;
    ...
  }
}

The database connection is asynchronous, but NestJS hides this process from the end user. The ProductRepository is awaiting a database connection, and the ProductService will be delayed until the repository is ready. When each class is instantiated, the entire application can begin. Here is the completed ProductModule:

import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { productProviders } from './product.providers';
import { ProductService } from './product.service';

@Module({
  imports: [DatabaseModule],
  providers: [
    ...productProviders,
    ProductService,
  ],
})
export class ProductModule {}

Finally, keep in mind that you must import the ProductModule into the root AppModule in order for NestJS to recognize it.

As you can see, implementing the repository pattern with the NestJS framework and TypeORM is quite different. Just remember the over-engineering moments I mentioned earlier, and keep in mind that NestJS is a pretty versatile framework that offers a variety of tools to help you map your data, such as MikroORM and even Mongoose, which is less dependent on any specific architecture. Keep the concept in mind and conduct some experiments. Personally, I’ve experimented with a custom Repository implementation in conjunction with Mongoose, and I think I prefer it. But I’ll stop with the examples here to save you from more headaches in just one blog post. 🙂