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 ์—ฐ๋™