--- name: repository-pattern description: 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. ```mermaid graph TB Controller["Controller
(HTTP Handler)"] --> Service["Service Layer
(Business Logic)"] Service --> Repository["Repository
(Data Access)"] Repository --> Prisma["Prisma Client
(ORM)"] Prisma --> Database[("Database
(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. ```mermaid classDiagram class BaseRepository { <> #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 { <> +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 ```typescript import { PrismaClient, User } from '@prisma/client'; import { BaseRepository } from '../modules/common/repository'; export class UserRepository extends BaseRepository { constructor(prisma: PrismaClient) { super(prisma, 'user'); } // Add custom methods async findByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { email }, }); } async findByUsername(username: string): Promise { return this.prisma.user.findUnique({ where: { username }, }); } } ``` ### Custom Query Methods Add domain-specific query methods: ```typescript export class UserRepository extends BaseRepository { // Find user with related data async findWithPermissions(userId: string): Promise { 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 { return this.prisma.user.findMany({ where: { isActive: true, ...(organizationId && { organizationId }), }, orderBy: { createdAt: 'desc' }, }); } } ``` ### Using Repository Interface Implement the `IRepository` interface for type safety: ```typescript import { IRepository } from '../modules/common/repository'; export class UserRepository extends BaseRepository implements IRepository { // Implementation... } ``` ### Error Handling BaseRepository handles errors automatically: ```typescript async findById(id: string): Promise { 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: ```typescript 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**: ```mermaid 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: ```typescript // 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 - [BaseRepository](../../services/iam-service/src/modules/common/repository.ts) - Base repository implementation - [User Repository](../../services/iam-service/src/repositories/user.repository.ts) - Example repository - [Database Prisma](../database-prisma/SKILL.md) - Prisma ORM patterns - [Error Handling](../error-handling-patterns/SKILL.md) - Error handling in repositories