--- id: pattern-repository title: Design Pattern - Repository category: design-patterns priority: high tags: [design-patterns, repository, data-access, separation-of-concerns] related: [solid-dip-abstractions, solid-srp-class, core-separation-concerns] --- # Repository Pattern The Repository pattern abstracts data persistence, providing a collection-like interface for accessing domain objects. It separates business logic from data access, makes code testable, and allows swapping storage implementations without changing application code. ## Bad Example ```typescript // ❌ Data access mixed with business logic class OrderService { async createOrder(userId: string, items: CartItem[]) { // Business logic const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0); // Direct database access - tightly coupled const result = await pool.query( `INSERT INTO orders (user_id, total, status, created_at) VALUES ($1, $2, $3, NOW()) RETURNING *`, [userId, total, 'pending'] ); const orderId = result.rows[0].id; // More SQL scattered in business logic for (const item of items) { await pool.query( `INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)`, [orderId, item.productId, item.quantity, item.price] ); } return result.rows[0]; } async getOrdersByUser(userId: string) { // SQL everywhere const result = await pool.query( `SELECT o.*, json_agg(oi.*) as items FROM orders o LEFT JOIN order_items oi ON o.id = oi.order_id WHERE o.user_id = $1 GROUP BY o.id`, [userId] ); return result.rows; } } ``` **Problems:** - Business logic mixed with SQL - Hard to test without real database - Changing database requires rewriting service - SQL scattered across codebase ## Good Example ### Define Repository Interface ```typescript // ✅ Repository interface - contract for data access interface OrderRepository { find(id: string): Promise; findByUser(userId: string): Promise; findByStatus(status: OrderStatus): Promise; save(order: Order): Promise; delete(id: string): Promise; } interface OrderItemRepository { findByOrder(orderId: string): Promise; saveMany(items: OrderItem[]): Promise; deleteByOrder(orderId: string): Promise; } ``` ### Implement Repository ```typescript // ✅ PostgreSQL implementation class PostgresOrderRepository implements OrderRepository { constructor(private db: Pool) {} async find(id: string): Promise { const result = await this.db.query( 'SELECT * FROM orders WHERE id = $1', [id] ); return result.rows[0] ? this.mapToOrder(result.rows[0]) : null; } async findByUser(userId: string): Promise { const result = await this.db.query( 'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC', [userId] ); return result.rows.map(this.mapToOrder); } async findByStatus(status: OrderStatus): Promise { const result = await this.db.query( 'SELECT * FROM orders WHERE status = $1', [status] ); return result.rows.map(this.mapToOrder); } async save(order: Order): Promise { if (order.id) { return this.update(order); } return this.insert(order); } private async insert(order: Order): Promise { const result = await this.db.query( `INSERT INTO orders (user_id, total, status, created_at) VALUES ($1, $2, $3, NOW()) RETURNING *`, [order.userId, order.total, order.status] ); return this.mapToOrder(result.rows[0]); } private async update(order: Order): Promise { const result = await this.db.query( `UPDATE orders SET total = $1, status = $2, updated_at = NOW() WHERE id = $3 RETURNING *`, [order.total, order.status, order.id] ); return this.mapToOrder(result.rows[0]); } async delete(id: string): Promise { await this.db.query('DELETE FROM orders WHERE id = $1', [id]); } private mapToOrder(row: any): Order { return new Order({ id: row.id, userId: row.user_id, total: parseFloat(row.total), status: row.status as OrderStatus, createdAt: row.created_at, updatedAt: row.updated_at, }); } } ``` ### Clean Service Layer ```typescript // ✅ Service focuses on business logic only class OrderService { constructor( private orderRepository: OrderRepository, private orderItemRepository: OrderItemRepository, private productRepository: ProductRepository, ) {} async createOrder(userId: string, items: CartItem[]): Promise { // Pure business logic const total = this.calculateTotal(items); const order = new Order({ userId, total, status: OrderStatus.Pending, }); // Repository handles persistence const savedOrder = await this.orderRepository.save(order); const orderItems = items.map(item => new OrderItem({ orderId: savedOrder.id, productId: item.productId, quantity: item.quantity, price: item.price, })); await this.orderItemRepository.saveMany(orderItems); return savedOrder; } async cancelOrder(orderId: string): Promise { const order = await this.orderRepository.find(orderId); if (!order) { throw new OrderNotFoundException(orderId); } if (!order.canBeCancelled()) { throw new InvalidOrderStateException('Order cannot be cancelled'); } order.cancel(); return this.orderRepository.save(order); } private calculateTotal(items: CartItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); } } ``` ### In-Memory Repository for Testing ```typescript // ✅ In-memory implementation for tests class InMemoryOrderRepository implements OrderRepository { private orders: Map = new Map(); private idCounter = 1; async find(id: string): Promise { return this.orders.get(id) ?? null; } async findByUser(userId: string): Promise { return Array.from(this.orders.values()) .filter(order => order.userId === userId); } async findByStatus(status: OrderStatus): Promise { return Array.from(this.orders.values()) .filter(order => order.status === status); } async save(order: Order): Promise { if (!order.id) { order.id = String(this.idCounter++); } this.orders.set(order.id, order); return order; } async delete(id: string): Promise { this.orders.delete(id); } // Test helper methods clear(): void { this.orders.clear(); } seed(orders: Order[]): void { orders.forEach(order => this.orders.set(order.id, order)); } } ``` ### Easy Testing ```typescript // ✅ Unit tests without database describe('OrderService', () => { let orderService: OrderService; let orderRepository: InMemoryOrderRepository; let orderItemRepository: InMemoryOrderItemRepository; beforeEach(() => { orderRepository = new InMemoryOrderRepository(); orderItemRepository = new InMemoryOrderItemRepository(); orderService = new OrderService( orderRepository, orderItemRepository, new InMemoryProductRepository(), ); }); it('creates order with calculated total', async () => { const items = [ { productId: '1', quantity: 2, price: 10 }, { productId: '2', quantity: 1, price: 25 }, ]; const order = await orderService.createOrder('user-1', items); expect(order.total).toBe(45); expect(order.status).toBe(OrderStatus.Pending); }); it('cancels pending order', async () => { orderRepository.seed([ new Order({ id: '1', userId: 'user-1', status: OrderStatus.Pending }), ]); const order = await orderService.cancelOrder('1'); expect(order.status).toBe(OrderStatus.Cancelled); }); it('throws when cancelling shipped order', async () => { orderRepository.seed([ new Order({ id: '1', userId: 'user-1', status: OrderStatus.Shipped }), ]); await expect(orderService.cancelOrder('1')) .rejects.toThrow(InvalidOrderStateException); }); }); ``` ### Generic Repository Base ```typescript // ✅ Generic base for common operations interface Repository { find(id: ID): Promise; findAll(): Promise; save(entity: T): Promise; delete(id: ID): Promise; exists(id: ID): Promise; } abstract class BasePostgresRepository implements Repository { constructor( protected db: Pool, protected tableName: string, ) {} async find(id: ID): Promise { const result = await this.db.query( `SELECT * FROM ${this.tableName} WHERE id = $1`, [id] ); return result.rows[0] ? this.mapToEntity(result.rows[0]) : null; } async exists(id: ID): Promise { const result = await this.db.query( `SELECT 1 FROM ${this.tableName} WHERE id = $1`, [id] ); return result.rows.length > 0; } protected abstract mapToEntity(row: any): T; } ``` ## Why - Separation of concerns - Business logic free of persistence details - Easy to test with in-memory implementations - Swap databases without changing services - Centralized query logic - Consistent data access patterns - Single place to add caching, logging