Skip to content
All essays
ArchitectureMarch 27, 20259 min

Repository Pattern: Abstract Data Access Logic

Master the Repository pattern. Separate data access logic from business logic and build maintainable applications.

Ü
Ümit Uz
Mobile & Full Stack Developer

Understanding the Repository Pattern

The Repository pattern mediates between the domain and data mapping layers, acting like an in-memory domain object collection. It abstracts data access logic, providing a collection-like interface for accessing domain objects.

typescript
// Without Repository
class UserService {
  async getUser(id: string) {
    return await db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

// With Repository
class UserRepository {
  findById(id: string): Promise<User> {
    // Data access logic
  }
}

class UserService {
  constructor(private repo: UserRepository) {}
  
  async getUser(id: string) {
    return this.repo.findById(id);
  }
}

Benefits

Separation of Concerns

typescript
// Business logic doesn't know about database
class OrderService {
  constructor(private orderRepo: OrderRepository) {}
  
  async placeOrder(items: Item[]) {
    const order = new Order(items);
    await this.orderRepo.save(order);
  }
}

Testability

typescript
// Easy to mock for testing
const mockRepo = {
  findById: jest.fn().mockResolvedValue(user)
};

const service = new UserService(mockRepo);

Implementation

Generic Repository

typescript
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(entity: T): Promise<T>;
  update(id: string, entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

class SqlRepository<T> implements Repository<T> {
  constructor(
    private tableName: string,
    private db: Database
  ) {}
  
  async findById(id: string): Promise<T | null> {
    const result = await this.db.query(
      `SELECT * FROM ${this.tableName} WHERE id = ?`,
      [id]
    );
    return result[0] || null;
  }
  
  async create(entity: T): Promise<T> {
    await this.db.insert(this.tableName, entity);
    return entity;
  }
}

Specific Repository

typescript
class UserRepository implements Repository<User> {
  constructor(private db: Database) {}
  
  async findByEmail(email: string): Promise<User | null> {
    const result = await this.db.query(
      'SELECT * FROM users WHERE email = ?',
      [email]
    );
    return result[0] || null;
  }
  
  async findById(id: string): Promise<User | null> {
    // Implementation
  }
  
  async create(user: User): Promise<User> {
    const hashedPassword = await bcrypt.hash(user.password, 10);
    await this.db.insert('users', {
      ...user,
      password: hashedPassword
    });
    return user;
  }
}

Best Practices

  1. 1Keep repositories focused on data access
  2. 2Return domain objects, not database entities
  3. 3Use interfaces for flexibility
  4. 4Implement error handling
  5. 5Add transaction support

Conclusion

The Repository pattern separates data access from business logic, improving testability and maintainability. Use it when you have complex data access logic.

Build clean, maintainable applications with the Repository pattern!

Enjoyed this essay? Share it.

Related essays

Next essay
Observer Pattern: Build Reactive Systems