Repository implementations live in the infrastructure layer and fulfill the contracts defined by the domain's repository interfaces. They are responsible for fetching data from data sources, converting DTOs to entities, and mapping all infrastructure exceptions to domain errors.
Repository implementations use the same base name as the interface without the I prefix:
// Domain interface
abstract interface class IUserRepository { ... }
// Infrastructure implementation
class UserRepository implements IUserRepository { ... }The repository receives its data sources as named, required constructor parameters. Dependencies are stored as final private fields:
class UserRepository implements IUserRepository {
UserRepository({
required IUserApi userApi,
}) : _userApi = userApi;
final IUserApi _userApi;
}Repositories depend on data source interfaces (
IUserApi), not on concrete implementations.
Repository implementations fulfill the TaskEither<E, T> (or Either<E, T>) contract defined by the domain interface. The repository is responsible for:
- Calling the data source.
- Converting the raw result to a DTO.
- Converting the DTO to a domain entity.
- Catching exceptions and returning the appropriate domain error as
Left.
@override
TaskEither<UserError, User> getUserById(String id) =>
TaskEither(() async {
try {
final map = await _userApi.getUser(id);
return right(UserDto.fromMap(map).toEntity());
} on NotFoundException {
return left(UserNotFoundError(id));
} on UnauthorizedException {
return left(UserNetworkError.unauthorized);
} catch (_) {
return left(UserNetworkError.serverError);
}
});Every exception thrown by a data source must be caught in the repository and converted to a domain error. Do not let raw exceptions propagate to the application or presentation layers.
Map exceptions to the most specific domain error available:
on NotFoundException => left(UserNotFoundError(id))
on UnauthorizedException => left(UserNetworkError.unauthorized)
on TimeoutException => left(UserNetworkError.timeout)
catch (_) => left(UserNetworkError.serverError)When the domain interface declares a Stream<T>, the implementation wraps a StreamController (or BehaviorSubject from rxdart) and exposes the stream as a getter:
class UserRepository implements IUserRepository {
UserRepository({required IUserApi userApi}) : _userApi = userApi;
final IUserApi _userApi;
final _currentUserController = BehaviorSubject<User?>();
@override
Stream<User?> get currentUserStream => _currentUserController.stream;
// Call _currentUserController.add(...) whenever the user changes.
void dispose() => _currentUserController.close();
}If the domain interface exposes a getter attribute (e.g., a cached value), implement it by wrapping a private field:
User? _cachedUser;
@override
User? get cachedUser => _cachedUser;// infrastructure/repositories/user_repository.dart
import 'package:fpdart/fpdart.dart';
import '../../domain/entities/user.dart';
import '../../domain/errors/user_error.dart';
import '../../domain/repositories/i_user_repository.dart';
import '../data_sources/user/i_user_api.dart';
import '../dtos/user_dto.dart';
class UserRepository implements IUserRepository {
UserRepository({required IUserApi userApi}) : _userApi = userApi;
final IUserApi _userApi;
@override
TaskEither<UserError, User> getCurrentUser() =>
TaskEither(() async {
try {
final map = await _userApi.getCurrentUser();
return right(UserDto.fromMap(map).toEntity());
} on UnauthorizedException {
return left(UserNetworkError.unauthorized);
} catch (_) {
return left(UserNetworkError.serverError);
}
});
@override
TaskEither<UserError, User> getUserById(String id) =>
TaskEither(() async {
try {
final map = await _userApi.getUser(id);
return right(UserDto.fromMap(map).toEntity());
} on NotFoundException {
return left(UserNotFoundError(id));
} on UnauthorizedException {
return left(UserNetworkError.unauthorized);
} catch (_) {
return left(UserNetworkError.serverError);
}
});
@override
TaskEither<UserError, Unit> updateUser(User user) =>
TaskEither(() async {
try {
await _userApi.updateUser(user.id, UserDto.fromEntity(user).toMap());
return right(unit);
} on UnauthorizedException {
return left(UserNetworkError.unauthorized);
} on ValidationException catch (e) {
return left(UserValidationError(field: e.field, message: e.message));
} catch (_) {
return left(UserNetworkError.serverError);
}
});
}