LogoSkills

Caching Strategy Patterns

Optimizes user experience and network efficiency through data caching strategies.

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

Optimizes user experience and network efficiency through data caching strategies. Refer to the /client-cache skill for detailed implementation guide.


Strategy Comparison#

StrategyEntry PointReturn TypeSuitable For
SWR watchStream() Stream<Either<Failure, T>> GET lists, externally mutable
CacheFirst execute() Future<Either<Failure, T>> GET single, offline first
NetworkFirst execute() Future<Either<Failure, T>> Payment/auth, always need latest

SWR Pattern (Stale-While-Revalidate)#

Returns cached data immediately and refreshes in the background.

Repository Interface#

// domain/repository/i_schedule_repository.dart
abstract interface class IScheduleRepository {
  /// Returns Stream using SWR strategy
  Stream<Either<Failure, List<ScheduleDayData>>> getScheduleData({
    required DateTime startDate,
    required DateTime endDate,
    String? classId,
  });
}

Repository Mixin (Strategy Assembly)#

mixin ScheduleOpenApiMixin implements IScheduleRepository {
  OpenApiService get openApiService;
  ScheduleItemDao get scheduleItemDao;

  @override
  Stream<Either<Failure, List<ScheduleDayData>>> getScheduleData({
    required DateTime startDate,
    required DateTime endDate,
    String? classId,
  }) =>
      SwrStrategyImpl<List<ScheduleDayData>>(
        cacheRepository: ScheduleDataCacheRepository(
          scheduleItemDao: scheduleItemDao,
        ),
        networkRepository: ScheduleDataNetworkRepository(
          openApiService: openApiService,
        ),
        policy: CachePolicies.standard,
      ).watchStream(
        ScheduleDataCacheQuery(
          startDate: startDate,
          endDate: endDate,
          classId: classId,
        ),
      );
}

BLoC Integration: emit.forEach + restartable()#

class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
  ScheduleBloc() : super(const ScheduleState()) {
    on<_LoadData>(_onLoadData, transformer: restartable());
  }

  Future<void> _onLoadData(_LoadData event, Emitter<ScheduleState> emit) async {
    await emit.forEach(
      const GetScheduleDataUseCase().call(
        GetScheduleDataParams(
          startDate: event.startDate,
          endDate: event.endDate,
        ),
      ),
      onData: (result) => result.fold(
        (failure) => state.copyWith(status: Status.failure),
        (data)    => state.copyWith(status: Status.success, data: data),
      ),
      onError: (_, __) => state.copyWith(status: Status.failure),
    );
  }
}

How restartable() Works

1. add(LoadData(start: mar1))  โ†’ ์ŠคํŠธ๋ฆผA ๊ตฌ๋… (์บ์‹œ โ†’ ์„œ๋ฒ„ ์ˆœ์ฐจ emit)
2. add(LoadData(start: apr1))  โ†’ restartable()๊ฐ€ ํ•ธ๋“ค๋Ÿฌ1 ์ทจ์†Œ โ†’ ์ŠคํŠธ๋ฆผA ํ•ด์ œ
                                  โ†’ ํ•ธ๋“ค๋Ÿฌ2 ์‹œ์ž‘ โ†’ ์ŠคํŠธ๋ฆผB ๊ตฌ๋…

emit.forEach๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ stream.listen()์„ Callํ•˜๋ฉฐ, restartable()๊ฐ€ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ทจ์†Œํ•˜๋ฉด ๊ตฌ๋…๋„ Auto ์ •๋ฆฌ๋ฉ๋‹ˆ๋‹ค. close() ์˜ค๋ฒ„๋ผ์ด๋“œ์™€ StreamSubscription ๊ด€๋ฆฌ๊ฐ€ Not neededํ•ฉ๋‹ˆ๋‹ค.


CacheFirst Pattern#

Returns immediately without network call if cache is valid. Makes network call if expired or missing.

Repository Mixin#

@override
Future<Either<Failure, ScheduleMemo?>> getMemo({
  required DateTime date,
}) =>
    CacheFirstStrategyImpl<ScheduleMemo?>(
      cacheRepository: ScheduleMemoCacheRepository(
        memoCacheDao: memoCacheDao,
      ),
      networkRepository: ScheduleMemoNetworkRepository(
        openApiService: openApiService,
      ),
      policy: CachePolicies.standard,
    ).execute(
      ScheduleMemoCacheQuery(date: date),
    );

BLoC Integration#

Future<void> _onDetailLoaded(
  _DetailLoaded event,
  Emitter<ScheduleState> emit,
) async {
  emit(state.copyWith(detailStatus: Status.loading));
  final result = await const GetMemoUseCase().call(
    GetMemoParams(date: event.date),
  );
  if (isClosed) return;  // Required
  result.fold(
    (failure) => emit(state.copyWith(detailStatus: Status.failure)),
    (memo)    => emit(state.copyWith(detailStatus: Status.success, memo: memo)),
  );
}

NetworkFirst Pattern#

Makes network call first and falls back to cache on failure.

Repository Mixin#

@override
Future<Either<Failure, PaymentStatus>> getPaymentStatus({
  required String orderId,
}) =>
    NetworkFirstStrategyImpl<PaymentStatus>(
      cacheRepository: PaymentStatusCacheRepository(
        paymentStatusDao: paymentStatusDao,
      ),
      networkRepository: PaymentStatusNetworkRepository(
        openApiService: openApiService,
      ),
      policy: CachePolicies.realtime,
    ).execute(
      PaymentStatusCacheQuery(orderId: orderId),
    );

Local Cache Implementation (Drift)#

Tables (Row tables only, JSON blob prohibited)#

@DataClassName('ScheduleItemData')
class ScheduleItems extends Table {
  TextColumn get cacheKey => text()();          // Required
  DateTimeColumn get date => dateTime()();      // Parent field
  TextColumn get id => text()();
  TextColumn get title => text()();
  DateTimeColumn get cachedAt => dateTime()();  // Required
  // No primaryKey (use rowid)
}

DAO (Drift CRUD only, no domain entity dependency)#

// DELETE(cacheKey) + INSERT (upsert prohibited)
Future<void> saveItems(
  List<ScheduleItemsCompanion> companions,
  String cacheKey,
) async {
  await (delete(scheduleItems)
        ..where((t) => t.cacheKey.equals(cacheKey)))
      .go();
  await batch((b) => b.insertAll(scheduleItems, companions));
}

// Cache invalidation (prefix matching)
Future<void> deleteByCacheKeyPrefix(String prefix) =>
    (delete(scheduleItems)
          ..where((t) => t.cacheKey.like('$prefix%')))
        .go();

Selection Guide#

HTTP Method?
โ”œโ”€โ”€ GET
โ”‚   โ”œโ”€โ”€ Response is list  โ†’  SWR
โ”‚   โ””โ”€โ”€ Response is single
โ”‚       โ”œโ”€โ”€ Offline first  โ†’  CacheFirst
โ”‚       โ””โ”€โ”€ Always latest  โ†’  NetworkFirst
โ””โ”€โ”€ POST/PUT/DELETE
    โ†’ Direct call + deleteByCacheKeyPrefix()

Suitable Cases by Strategy#

SWRCacheFirstNetworkFirst
Feed, timelineUser profilePayment status
Notification listApp settingsAuth token
Schedule, homework listCategory listReal-time balance

Checklist#

  • Select caching strategy (SWR / CacheFirst / NetworkFirst)
  • Drift table: row table, cacheKey + cachedAt, no primaryKey
  • DAO: Drift CRUD only (no domain entity dependency, Companion only)
  • CacheQuery: 1:1 mapping with UseCase
  • CacheRepository: _toEntity, _toCompanion conversion methods
  • NetworkRepository: Use OpenApiService
  • BLoC SWR: emit.forEach + restartable() + onError
  • BLoC write: await + isClosed + droppable()
  • Invalidate related caches after write with deleteByCacheKeyPrefix()

Referencing Agents#

  • /feature:data - Data Layer caching implementation
  • /feature:bloc - Stream-integrated BLoC
  • /client-cache - Detailed implementation guide skill