home / skills / kaakati / rails-enterprise-dev / repository-patterns

This skill helps implement offline-first repository patterns in Dart/Flutter projects, enabling robust data access with cache, network fallbacks, and automated

npx playbooks add skill kaakati/rails-enterprise-dev --skill repository-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.1 KB
---
name: "Repository Patterns"
description: "Repository interface and implementation patterns with offline-first strategies"
version: "1.0.0"
---

# Repository Patterns

## Repository Interface (Domain Layer)

```dart
// lib/domain/repositories/user_repository.dart
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../entities/user.dart';

abstract class UserRepository {
  Future<Either<Failure, User>> getUser(String id);
  Future<Either<Failure, List<User>>> getAllUsers();
  Future<Either<Failure, User>> createUser(User user);
  Future<Either<Failure, User>> updateUser(User user);
  Future<Either<Failure, void>> deleteUser(String id);
}
```

## Repository Implementation (Data Layer)

### Basic Implementation

```dart
// lib/data/repositories/user_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../core/errors/failures.dart';
import '../../core/errors/exceptions.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../providers/user_provider.dart';

class UserRepositoryImpl implements UserRepository {
  final UserProvider _provider;
  
  UserRepositoryImpl(this._provider);
  
  @override
  Future<Either<Failure, User>> getUser(String id) async {
    try {
      final model = await _provider.fetchUser(id);
      return Right(model.toEntity());
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    } on NetworkException {
      return Left(NetworkFailure());
    } catch (e) {
      return Left(ServerFailure('Unexpected error: $e'));
    }
  }
}
```

### Offline-First Implementation

```dart
class UserRepositoryImpl implements UserRepository {
  final UserProvider _remoteSource;
  final UserLocalSource _localSource;
  final NetworkInfo _networkInfo;
  
  UserRepositoryImpl(
    this._remoteSource,
    this._localSource,
    this._networkInfo,
  );
  
  @override
  Future<Either<Failure, User>> getUser(String id) async {
    if (await _networkInfo.isConnected) {
      // Try remote first
      try {
        final model = await _remoteSource.fetchUser(id);
        // Cache for offline use
        await _localSource.cacheUser(model);
        return Right(model.toEntity());
      } on ServerException catch (e) {
        // Fallback to cache on server error
        return _getCachedUser(id);
      }
    } else {
      // Use cache when offline
      return _getCachedUser(id);
    }
  }
  
  Future<Either<Failure, User>> _getCachedUser(String id) async {
    try {
      final cached = await _localSource.getCachedUser(id);
      if (cached != null) {
        return Right(cached.toEntity());
      } else {
        return Left(CacheFailure('No cached data available'));
      }
    } on CacheException catch (e) {
      return Left(CacheFailure(e.message));
    }
  }
  
  @override
  Future<Either<Failure, List<User>>> getAllUsers() async {
    if (await _networkInfo.isConnected) {
      try {
        final models = await _remoteSource.fetchAllUsers();
        await _localSource.cacheUsers(models);
        return Right(models.map((m) => m.toEntity()).toList());
      } on ServerException {
        return _getCachedUsers();
      }
    } else {
      return _getCachedUsers();
    }
  }
  
  Future<Either<Failure, List<User>>> _getCachedUsers() async {
    try {
      final cached = await _localSource.getCachedUsers();
      if (cached != null && cached.isNotEmpty) {
        return Right(cached.map((m) => m.toEntity()).toList());
      } else {
        return Left(CacheFailure('No cached users'));
      }
    } on CacheException catch (e) {
      return Left(CacheFailure(e.message));
    }
  }
}
```

## Caching Strategies

### 1. Cache-First (Offline-First)
```dart
// Try cache first, then network
Future<Either<Failure, User>> getUser(String id) async {
  // Check cache first
  final cached = await _localSource.getCachedUser(id);
  if (cached != null) {
    // Return cached data immediately
    _refreshInBackground(id); // Update in background
    return Right(cached.toEntity());
  }
  
  // Cache miss - fetch from network
  return _fetchFromNetwork(id);
}
```

### 2. Network-First (Fresh Data Priority)
```dart
// Try network first, fallback to cache
Future<Either<Failure, User>> getUser(String id) async {
  if (await _networkInfo.isConnected) {
    try {
      final model = await _remoteSource.fetchUser(id);
      await _localSource.cacheUser(model);
      return Right(model.toEntity());
    } catch (e) {
      return _getCachedUser(id); // Fallback
    }
  } else {
    return _getCachedUser(id);
  }
}
```

### 3. Cache-Then-Network
```dart
// Return cache immediately, then update with network data
Stream<Either<Failure, User>> getUserStream(String id) async* {
  // Emit cached data first
  final cached = await _localSource.getCachedUser(id);
  if (cached != null) {
    yield Right(cached.toEntity());
  }
  
  // Then fetch from network
  if (await _networkInfo.isConnected) {
    try {
      final model = await _remoteSource.fetchUser(id);
      await _localSource.cacheUser(model);
      yield Right(model.toEntity());
    } on ServerException catch (e) {
      yield Left(ServerFailure(e.message));
    }
  }
}
```

Overview

This skill documents repository interface and implementation patterns for clean, testable data layers with offline-first strategies. It explains domain repository contracts, concrete data implementations, and several caching strategies (cache-first, network-first, cache-then-network) tailored for resilient mobile and web clients. The guidance is generic and portable across projects that need predictable data flow and offline support.

How this skill works

The skill outlines a domain-level repository interface that returns Either<Failure, T> results, decoupling callers from concrete data sources and errors. It shows basic implementations that map provider exceptions to failure types and offline-first implementations that coordinate remote sources, local caches, and network connectivity. It also demonstrates concrete caching strategies and helper flows (fallbacks, background refresh, streams) to deliver fresh and available data.

When to use it

  • You need a single abstraction for data operations across remote and local sources.
  • Clients must handle network variability and provide offline access or graceful degradation.
  • You want clear failure handling and predictable error propagation for business logic tests.
  • You need to cache frequently accessed entities and keep local copies synchronized with remote data.
  • You are building mobile or edge clients that require immediate data responses and background refresh.

Best practices

  • Define thin repository interfaces in the domain layer returning Either/Result types to surface failures explicitly.
  • Centralize exception-to-failure mapping in the data layer to keep domain logic pure and testable.
  • Prefer dependency injection for remote providers, local sources, and network info for easy mocking in tests.
  • Choose a caching strategy based on UX: cache-first for instant reads, network-first for freshest data, and cache-then-network for progressive updates.
  • Always cache successful remote responses and handle cache exceptions with clear failure types to avoid silent drops.
  • Implement background refresh and stream-based updates where UI needs progressive improvement without blocking.

Example use cases

  • User profile retrieval: return cached profile instantly and refresh in background for updates.
  • List views: cache list pages locally for offline browsing and sync when connectivity returns.
  • Create/update operations: apply optimistic local changes and reconcile with remote on network availability.
  • Analytics or telemetry buffering: store events locally and flush to remote when connected.
  • Search with suggestions: show cached suggestions immediately, then update with remote results asynchronously.

FAQ

How do I choose between cache-first and network-first?

Pick cache-first when fast UI responses and offline access are priorities; pick network-first when data freshness is critical and stale data is unacceptable.

What if cache and network both fail?

Return a clear CacheFailure or ServerFailure from the repository and let the presentation layer show retries, offline UI, or recovery flows.