Skip to main content

Repository Interfaces

Repository interfaces define contracts for data persistence operations. They belong to the domain layer but are implemented in the infrastructure layer, following the Dependency Inversion Principle.

Overview

Repositories provide an abstraction over data access, allowing the domain layer to remain independent of specific persistence technologies (e.g., MongoDB, PostgreSQL).
The repository pattern follows these principles:
  1. Interface in Domain Layer: Defines what operations are needed
  2. Implementation in Infrastructure Layer: Provides concrete implementation
  3. Dependency Injection: Use cases depend on interfaces, not implementations
// Domain Layer
export interface ICustomerRepository {
  create(customer: Customer): Promise<Customer>;
}

// Infrastructure Layer
export class MongoCustomerRepository implements ICustomerRepository {
  async create(customer: Customer): Promise<Customer> {
    // MongoDB-specific implementation
  }
}

// Application Layer
export class CreateOrderUseCase {
  constructor(
    private readonly customerRepo: ICustomerRepository // Depends on interface
  ) {}
}

ICustomerRepository

Interface for managing customer data persistence.

Interface Definition

src/domain/repositories/ICustomerRepository.ts
import { Customer } from "../entities/Customer";

export interface ICustomerRepository {
  create(customer: Customer): Promise<Customer>;
  findByEmail(email: string): Promise<Customer | null>;
  findById(id: string): Promise<Customer | null>;
}

Methods

create(customer: Customer): Promise<Customer>
Persists a new customer to the database.
customer
Customer
required
Customer entity to persist (without id field)
Returns: Promise<Customer> - The created customer with generated idExample:
import { createCustomer } from '@/domain/entities/Customer';

const customerData = createCustomer({
  name: 'John Doe',
  email: '[email protected]',
  phone: '1234567890',
});

const customer = await customerRepo.create(customerData);
console.log(customer.id); // Generated ID: "507f1f77bcf86cd799439011"
findByEmail(email: string): Promise<Customer | null>
Retrieves a customer by their email address.
email
string
required
Email address to search for
Returns: Promise<Customer | null> - The customer if found, null otherwiseExample:
const customer = await customerRepo.findByEmail('[email protected]');

if (customer) {
  console.log(`Found customer: ${customer.name}`);
} else {
  console.log('Customer not found');
}
findById(id: string): Promise<Customer | null>
Retrieves a customer by their unique identifier.
id
string
required
Customer ID to search for
Returns: Promise<Customer | null> - The customer if found, null otherwiseExample:
const customer = await customerRepo.findById('507f1f77bcf86cd799439011');

if (customer) {
  console.log(`Found customer: ${customer.name}`);
} else {
  console.log('Customer not found');
}

Implementation Reference

The MongoDB implementation can be found at:
  • src/infrastructure/persistence/mongodb/MongoCustomerRepository.ts

IOrderRepository

Interface for managing order data persistence.

Interface Definition

src/domain/repositories/IOrderRepository.ts
import { Order } from "../entities/Order";

export interface IOrderRepository {
  create(order: Order): Promise<Order>;
  findByCustomerId(customerId: string): Promise<Order[]>;
  findByReceiptId(receiptId: string): Promise<Order | null>;
}

Methods

create(order: Order): Promise<Order>
Persists a new order to the database.
order
Order
required
Order entity to persist (without id field)
Returns: Promise<Order> - The created order with generated idExample:
import { createOrder } from '@/domain/entities/Order';

const orderData = createOrder({
  customerId: 'cust_123',
  planId: 'plan-2',
  devices: 2,
  months: 6,
  amount: 70,
  paymentMethod: 'stripe',
  paymentReceiptId: 'pi_abc123',
  status: 'completed',
});

const order = await orderRepo.create(orderData);
console.log(order.id); // Generated ID: "507f1f77bcf86cd799439011"
findByCustomerId(customerId: string): Promise<Order[]>
Retrieves all orders for a specific customer.
customerId
string
required
Customer ID to search for
Returns: Promise<Order[]> - Array of orders (empty array if none found)Example:
const orders = await orderRepo.findByCustomerId('cust_123');

console.log(`Found ${orders.length} orders`);
orders.forEach(order => {
  console.log(`Order ${order.id}: $${order.amount}`);
});
findByReceiptId(receiptId: string): Promise<Order | null>
Retrieves an order by payment receipt ID. Useful for payment webhook processing.
receiptId
string
required
Payment gateway receipt/transaction ID
Returns: Promise<Order | null> - The order if found, null otherwiseExample:
// In a payment webhook handler
const receiptId = 'pi_abc123'; // From Stripe/PayPal
const order = await orderRepo.findByReceiptId(receiptId);

if (order) {
  console.log(`Order ${order.id} found for receipt ${receiptId}`);
  // Update order status, send confirmation email, etc.
} else {
  console.log('Order not found for this receipt');
}

Implementation Reference

The MongoDB implementation can be found at:
  • src/infrastructure/persistence/mongodb/MongoOrderRepository.ts

Repository Design Patterns

Repositories are injected into use cases via constructor injection:
export class CreateOrderUseCase {
  constructor(
    private readonly customerRepo: ICustomerRepository,
    private readonly orderRepo: IOrderRepository
  ) {}
}

// In dependency injection container
const customerRepo = new MongoCustomerRepository(db);
const orderRepo = new MongoOrderRepository(db);
const useCase = new CreateOrderUseCase(customerRepo, orderRepo);
This allows:
  • Testing with mock repositories
  • Switching implementations without changing use cases
  • Following SOLID principles
Repository methods should throw errors for exceptional cases:
class MongoCustomerRepository implements ICustomerRepository {
  async create(customer: Customer): Promise<Customer> {
    try {
      const result = await this.collection.insertOne(customer);
      return { ...customer, id: result.insertedId.toString() };
    } catch (error) {
      throw new Error(`Failed to create customer: ${error.message}`);
    }
  }
}
Use cases should handle repository errors:
try {
  const customer = await this.customerRepo.create(customerData);
} catch (error) {
  console.error('Failed to persist customer', error);
  throw error; // Or handle appropriately
}
Mock repositories for unit testing use cases:
import { ICustomerRepository } from '@/domain/repositories/ICustomerRepository';

class MockCustomerRepository implements ICustomerRepository {
  private customers: Customer[] = [];

  async create(customer: Customer): Promise<Customer> {
    const newCustomer = { ...customer, id: 'mock-id' };
    this.customers.push(newCustomer);
    return newCustomer;
  }

  async findByEmail(email: string): Promise<Customer | null> {
    return this.customers.find(c => c.email === email) || null;
  }

  async findById(id: string): Promise<Customer | null> {
    return this.customers.find(c => c.id === id) || null;
  }
}

// In tests
const mockCustomerRepo = new MockCustomerRepository();
const mockOrderRepo = new MockOrderRepository();
const useCase = new CreateOrderUseCase(mockCustomerRepo, mockOrderRepo);
Repository methods follow these conventions:
  • Create operations: Return the created entity with generated id
  • Find single: Return Entity | null (null if not found)
  • Find multiple: Return Entity[] (empty array if none found)
  • Update/Delete: Return void or boolean indicating success
interface IRepository<T> {
  create(entity: T): Promise<T>;           // Returns created entity
  findById(id: string): Promise<T | null>; // null if not found
  findAll(): Promise<T[]>;                 // Empty array if none
  update(entity: T): Promise<void>;        // void on success
  delete(id: string): Promise<boolean>;    // true if deleted
}

Build docs developers (and LLMs) love