LogoSkills

Repository Patterns

Repository abstracts data sources and handles data access from the Domain Layer.

Reference location: .claude/references/patterns/repository-patterns.md

Repository abstracts data sources and handles data access from the Domain Layer.


Pattern Comparison#

PatternProsConsRecommended For
Mixin Pattern Network logic separation, reusability Increased structural complexity Multiple data sources, complex APIs
Direct Implementation Simple and clear Difficult logic reuse Simple CRUD

Repository Interface (Domain Layer)#

// domain/repository/i_user_repository.dart
import 'package:dependencies/dependencies.dart';

abstract interface class IUserRepository {
  Future<Either<Failure, User>> getUser(int id);
  Future<Either<Failure, List<User>>> getUsers();
  Future<Either<Failure, User>> createUser(CreateUserParams params);
  Future<Either<Failure, User>> updateUser(UpdateUserParams params);
  Future<Either<Failure, Unit>> deleteUser(int id);

  // SWR 패턴용 Stream (캐시 전략은 /client-cache skill reference)
  Stream<Either<Failure, List<User>>> getUsersAsStream();
}

Repository Implementation#

// data/repository/user_repository.dart
import 'package:dependencies/dependencies.dart';

@LazySingleton(as: IUserRepository)
class UserRepository
    with UserServerpodMixin  // 네트워크 로직 분리
    implements IUserRepository {

  UserRepository(this._serverpodService);

  final ServerpodService _serverpodService;

  @override
  ServerpodClient get client => _serverpodService.client;
}

Serverpod Mixin (Network Logic)#

// data/repository/mixins/user_serverpod_mixin.dart
import 'package:serverpod_service/serverpod_service.dart' as serverpod;

mixin UserServerpodMixin implements IUserRepository {
  serverpod.ServerpodClient get client;

  @override
  Future<Either<Failure, User>> getUser(int id) async {
    try {
      final dto = await client.users.getUser(id);
      if (dto == null) {
        return left(NotFoundFailure('User not found: $id'));
      }
      return right(dto.toEntity());
    } on serverpod.ServerpodClientException catch (e, st) {
      return left(ServerFailure(e.message, error: e, stackTrace: st));
    }
  }

  @override
  Future<Either<Failure, List<User>>> getUsers() async {
    try {
      final dtos = await client.users.getUsers();
      return right(dtos.map((dto) => dto.toEntity()).toList());
    } on serverpod.ServerpodClientException catch (e, st) {
      return left(ServerFailure(e.message, error: e, stackTrace: st));
    }
  }

  @override
  Future<Either<Failure, User>> createUser(CreateUserParams params) async {
    try {
      final dto = await client.users.createUser(
        name: params.name,
        email: params.email,
      );
      return right(dto.toEntity());
    } on serverpod.ServerpodClientException catch (e, st) {
      return left(ServerFailure(e.message, error: e, stackTrace: st));
    }
  }
}

Reason for Using Namespaces#

  • Prevents name collisions between Domain Entity and API DTO (User, Category, etc.)
  • Clearly distinguished with serverpod. namespace to maintain Clean Architecture principles

Pattern B: Direct Implementation Pattern#

// data/repository/user_repository.dart
import 'package:dependencies/dependencies.dart';

@LazySingleton(as: IUserRepository)
class UserRepository implements IUserRepository {
  UserRepository(this._serverpodService);

  final ServerpodService _serverpodService;

  @override
  Future<Either<Failure, User>> getUser(int id) async {
    try {
      final response = await _serverpodService.client.users.getUser(id);
      if (response == null) {
        return left(NotFoundFailure('User not found: $id'));
      }
      return right(response.toEntity());
    } on Exception catch (e, st) {
      return left(ServerFailure(e.toString(), error: e, stackTrace: st));
    }
  }

  @override
  Future<Either<Failure, List<User>>> getUsers() async {
    try {
      final response = await _serverpodService.client.users.getUsers();
      return right(response.map((dto) => dto.toEntity()).toList());
    } on Exception catch (e, st) {
      return left(ServerFailure(e.toString(), error: e, stackTrace: st));
    }
  }
}

Structure#

data/repository/
├── user_repository.dart          # Repository 구현체
└── mixins/
    └── user_serverpod_mixin.dart # 네트워크 로직 (Mixin 패턴)

DTO to Entity Conversion#

// data/mapper/user_mapper.dart (or extension)
extension UserDtoMapper on serverpod.User {
  User toEntity() {
    return User(
      id: id!,
      name: name,
      email: email,
      avatarUrl: avatarUrl,
      createdAt: createdAt,
    );
  }
}

Selection Guide#

Need network logic reuse? ─Yes→ Mixin 패턴 (권장)
                         └No→ Multiple data source combination? ─Yes→ Mixin 패턴
                                                   └No→ Direct Implementation

Checklist#

  • Define Repository Interface (Domain Layer)
  • Create Repository implementation (Data Layer)
  • Implement Serverpod Mixin (when Mixin pattern selected)
  • Implement DTO to Entity conversion logic
  • Return Either<Failure, Success>
  • Exception handling (try-catch)
  • @LazySingleton annotation

Referencing Agents#

  • /feature:data - Data Layer Generation
  • /feature:domain - Repository Interface Definition
  • /serverpod:endpoint - Serverpod Endpoint 연동