Files
pos-system/docs/en/skills/repository-pattern.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes.
- Introduced configuration loading flow in configuration-management skill to clarify the configuration process.
- Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers.
- Enhanced various skills with additional diagrams to improve understanding of complex concepts.

These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
2026-01-01 23:22:54 +07:00

11 KiB

name, description
name description
repository-pattern Repository pattern implementation and best practices for GoodGo microservices. Use when implementing data access layers, extending BaseRepository, writing database queries, handling transactions, or optimizing database operations.

Repository Pattern

When to Use This Skill

Use this skill when:

  • Implementing data access layers for new modules
  • Extending BaseRepository for specific entity types
  • Writing custom database queries
  • Handling database transactions
  • Optimizing database queries and operations
  • Testing repository implementations
  • Organizing data access code

Core Concepts

Repository Pattern Benefits

  1. Abstraction: Separates business logic from data access
  2. Testability: Easy to mock repositories for testing
  3. Maintainability: Centralized database operations
  4. Consistency: Standardized CRUD operations
  5. Type Safety: TypeScript generics provide type safety

Repository Architecture

The repository pattern creates an abstraction layer between the service layer and data access layer, providing a clean separation of concerns.

graph TB
    Controller["Controller<br/>(HTTP Handler)"] --> Service["Service Layer<br/>(Business Logic)"]
    Service --> Repository["Repository<br/>(Data Access)"]
    Repository --> Prisma["Prisma Client<br/>(ORM)"]
    Prisma --> Database[("Database<br/>(PostgreSQL)")]
    
    style Controller fill:#e1f5ff
    style Service fill:#fff4e1
    style Repository fill:#e8f5e9
    style Prisma fill:#f3e5f5
    style Database fill:#ffebee

BaseRepository Class

The BaseRepository abstract class provides common database operations that can be extended:

  • findById(id) - Find entity by ID
  • findByUnique(field, value) - Find by unique field
  • findAll(options) - Find all with filtering, pagination, sorting
  • create(data) - Create new entity
  • update(id, data) - Update entity
  • delete(id) - Delete entity
  • count(where) - Count entities
  • exists(id) - Check if entity exists
  • transaction(callback) - Execute transaction

Class Hierarchy

Repositories extend the BaseRepository abstract class, inheriting common CRUD operations while allowing custom query methods.

classDiagram
    class BaseRepository {
        <<abstract>>
        #prisma: PrismaClient
        #modelName: string
        +findById(id: string) Promise~T~null~
        +findByUnique(field: string, value: any) Promise~T~null~
        +findAll(options?: any) Promise~T[]~
        +create(data: CreateInput) Promise~T~
        +update(id: string, data: UpdateInput) Promise~T~
        +delete(id: string) Promise~boolean~
        +count(where?: any) Promise~number~
        +exists(id: string) Promise~boolean~
        +transaction(callback: Function) Promise~R~
    }
    
    class IRepository {
        <<interface>>
        +findById(id: string) Promise~T~null~
        +findByUnique(field: string, value: any) Promise~T~null~
        +findAll(options?: any) Promise~T[]~
        +create(data: CreateInput) Promise~T~
        +update(id: string, data: UpdateInput) Promise~T~
        +delete(id: string) Promise~boolean~
        +count(where?: any) Promise~number~
        +exists(id: string) Promise~boolean~
    }
    
    class UserRepository {
        +findByEmail(email: string) Promise~User~null~
        +findByUsername(username: string) Promise~User~null~
        +findWithPermissions(userId: string) Promise~User~null~
        +findActiveUsers(organizationId?: string) Promise~User[]~
    }
    
    class ProductRepository {
        +findByCategory(categoryId: string) Promise~Product[]~
        +findActiveProducts() Promise~Product[]~
    }
    
    BaseRepository <|-- UserRepository : extends
    BaseRepository <|-- ProductRepository : extends
    IRepository <|.. UserRepository : implements

Note: Specific repositories like UserRepository and ProductRepository extend BaseRepository and can implement the IRepository interface for additional type safety. They inherit all base CRUD methods and add domain-specific query methods.

Patterns

Extending BaseRepository

import { PrismaClient, User } from '@prisma/client';
import { BaseRepository } from '../modules/common/repository';

export class UserRepository extends BaseRepository<User, CreateUserInput, UpdateUserInput> {
  constructor(prisma: PrismaClient) {
    super(prisma, 'user');
  }

  // Add custom methods
  async findByEmail(email: string): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { email },
    });
  }

  async findByUsername(username: string): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { username },
    });
  }
}

Custom Query Methods

Add domain-specific query methods:

export class UserRepository extends BaseRepository<User, CreateUserInput, UpdateUserInput> {
  // Find user with related data
  async findWithPermissions(userId: string): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { id: userId },
      include: {
        userRoles: {
          include: { role: true },
        },
        userPermissions: {
          include: { permission: true },
        },
      },
    });
  }

  // Complex query with filtering
  async findActiveUsers(organizationId?: string): Promise<User[]> {
    return this.prisma.user.findMany({
      where: {
        isActive: true,
        ...(organizationId && { organizationId }),
      },
      orderBy: { createdAt: 'desc' },
    });
  }
}

Using Repository Interface

Implement the IRepository interface for type safety:

import { IRepository } from '../modules/common/repository';

export class UserRepository 
  extends BaseRepository<User, CreateUserInput, UpdateUserInput>
  implements IRepository<User, CreateUserInput, UpdateUserInput> {
  
  // Implementation...
}

Error Handling

BaseRepository handles errors automatically:

async findById(id: string): Promise<T | null> {
  try {
    // Database operation
    const entity = await this.prisma.user.findUnique({ where: { id } });
    return entity;
  } catch (error: any) {
    logger.error(`Failed to find ${this.modelName} by ID`, { error, id });
    throw new DatabaseError(`Failed to find ${this.modelName}`, { id, originalError: error });
  }
}

Transactions

Use repository transaction method for multiple operations:

await repository.transaction(async (tx) => {
  const user = await tx.user.create({ data: userData });
  await tx.userProfile.create({ data: { userId: user.id, ...profileData } });
  return user;
});

Transaction Flow:

sequenceDiagram
    participant Service
    participant Repository
    participant Prisma as Prisma Client
    participant DB as Database
    
    Service->>Repository: transaction(callback)
    Repository->>Prisma: $transaction(callback)
    Prisma->>DB: BEGIN TRANSACTION
    
    Note over Service,DB: All operations use transaction client (tx)
    
    Service->>Repository: tx.user.create(data)
    Repository->>Prisma: tx.user.create(data)
    Prisma->>DB: INSERT INTO users ...
    DB-->>Prisma: User created
    Prisma-->>Repository: User entity
    Repository-->>Service: User entity
    
    Service->>Repository: tx.userProfile.create(data)
    Repository->>Prisma: tx.userProfile.create(data)
    Prisma->>DB: INSERT INTO user_profiles ...
    DB-->>Prisma: Profile created
    Prisma-->>Repository: Profile entity
    Repository-->>Service: Profile entity
    
    alt All operations succeed
        Prisma->>DB: COMMIT
        DB-->>Prisma: Transaction committed
        Prisma-->>Repository: Success result
        Repository-->>Service: Success result
    else Error occurs
        Prisma->>DB: ROLLBACK
        DB-->>Prisma: Transaction rolled back
        Prisma-->>Repository: Error thrown
        Repository-->>Service: DatabaseError thrown
    end

Important: All operations within the transaction callback must use the transaction client (tx) parameter, not the main Prisma client, to ensure atomicity.

Query Options

Use Prisma query options in findAll:

// Pagination
const users = await userRepository.findAll({
  skip: (page - 1) * limit,
  take: limit,
});

// Filtering
const activeUsers = await userRepository.findAll({
  where: { isActive: true },
});

// Sorting
const recentUsers = await userRepository.findAll({
  orderBy: { createdAt: 'desc' },
});

// Including relations
const usersWithRoles = await userRepository.findAll({
  include: { userRoles: true },
});

Best Practices

  1. Extend BaseRepository: Always extend BaseRepository instead of implementing from scratch
  2. Custom Methods: Add domain-specific query methods in repository subclasses
  3. Type Safety: Use TypeScript generics for type safety
  4. Error Handling: Let BaseRepository handle common errors, handle domain-specific errors in custom methods
  5. Logging: BaseRepository handles logging automatically
  6. Transactions: Use repository transaction method for multi-step operations
  7. Query Optimization: Use Prisma query options (select, include) to optimize queries
  8. Single Responsibility: Each repository handles one entity type
  9. Dependency Injection: Inject PrismaClient in constructor for testability

Common Mistakes

  1. Not Extending BaseRepository: Implementing CRUD from scratch instead of extending
  2. Business Logic in Repository: Putting business logic in repository instead of service layer
  3. Exposing Prisma Client: Exposing raw Prisma client instead of using repository methods
  4. Missing Error Handling: Not handling errors in custom query methods
  5. Over-fetching Data: Using include unnecessarily, fetching too much data
  6. No Type Safety: Not using TypeScript generics properly
  7. Transaction Mistakes: Not using repository transaction method for related operations

Troubleshooting

Type Errors with Prisma

Problem: TypeScript errors when using Prisma client methods Solution: Ensure Prisma client is generated: pnpm prisma generate. Use proper type assertions if needed.

Transaction Rollback Issues

Problem: Transaction not rolling back on error Solution: Ensure all operations in transaction callback use the transaction client (tx) parameter, not the main Prisma client.

Performance Issues

Problem: Slow queries or N+1 query problems Solution: Use include to fetch related data in single query. Use select to limit fields. Add database indexes.

Resources