Client cache architecture rules for this project.
Refer to the /client-cache skill for detailed implementation guide.
3 Cache Strategies#
| Strategy | UseCase Return Type | Entry Point | Selection Criteria |
|---|---|---|---|
| SWR | Stream<Either<Failure, T>> |
watchStream() |
GET lists, real-time, externally mutable |
| CacheFirst | Future<Either<Failure, T>> |
execute() |
GET single, offline first |
| NetworkFirst | Future<Either<Failure, T>> |
execute() |
Payment/auth, always need latest |
Automatic Strategy Selection Rules#
GET ๋ชฉ๋ก โ SWR โ Stream < Either < Failure, List < T > > >
GET ๋จ์ผ โ CacheFirst or NetworkFirst โ Future < Either < Failure, T > >
POST/PUT/DELETE โ ์ง์ ํธ์ถ (์บ์ ์์) + ๊ด๋ จ ์บ์ ๋ฌดํจํ
BLoC Transformer Rules#
| Event Type | transformer | Reason |
|---|---|---|
| SWR Stream subscription | restartable() |
Cancel previous Stream on parameter change |
All events using emit.forEach |
restartable() |
Prevent duplicate subscriptions |
| Write operations (POST/PUT/DELETE) | droppable() |
Prevent duplicate submissions |
| Independent parallel requests | concurrent() (default, can omit) |
Complete independently |
// โ
CORRECT: Apply restartable to SWR events
on<ItemsLoad>(_onLoad, transformer: restartable());
// โ
CORRECT: Apply droppable to write events
on<ItemCreate>(_onCreate, transformer: droppable());
emit.forEach Rules#
When integrating with SWR UseCase (call() returns Stream), always
use emit.forEach.
Manual StreamSubscription management prohibited. onError callback required.
// โ
CORRECT: emit.forEach + onError
Future<void> _onLoad(ItemsLoad event, Emitter<ItemsState> emit) async {
await emit.forEach(
const GetItemsUseCase().call(GetItemsParams(...)),
onData: (result) => result.fold(
(failure) => state.copyWith(status: LoadingStatus.error, failure: failure),
(items) => state.copyWith(status: LoadingStatus.loaded, items: items),
),
onError: (_, __) => state.copyWith(status: LoadingStatus.error),
);
}
// โ WRONG: Manual StreamSubscription management prohibited
StreamSubscription? _subscription;
// โ WRONG: .last loses SWR benefits (ignores cache)
final result = await useCase.call(params).last;
// โ WRONG: .first ignores server refresh (receives cache only)
final result = await useCase.call(params).first;
Anti-pattern details: See
swr-pattern.mdยง8
cacheKey Format#
' {domain}__{usecase}__key:value '
- Separator: double underscore (
__) -
Pagination parameters (
page,size,limit,offset) not included - Filter parameters are nullable, included only when
!= null - Date: Use
CacheQuery.fmtDate()(yyyy-MM-dd)
Example:
'schedule__get_schedule_data__start:2026-03-01__end:2026-03-31__class:5566'
'homework__get_homework_list__classId:42'
TTL (Cache Expiration Time)#
| Policy | TTL | Suitable For |
|---|---|---|
CachePolicies.realtime | 5 min | Notifications, attendance |
CachePolicies.standard | 10 min | Homework, schedule (default) |
CachePolicies.stable | 1 hour | Settings, products |
CachePolicies.immutable | 24 hours | Notices, terms |
Drift Table Rules#
Requirements#
- Row tables only (JSON blob prohibited)
- Do not set
primaryKey(use rowid) cacheKey+cachedAtcolumns required- Column names must match domain entity field names
- For nested structures, include parent entity fields as columns in each row
// โ
CORRECT: ํ ํ
์ด๋ธ, primaryKey ์์
class HomeworkItems extends Table {
TextColumn get cacheKey => text()();
TextColumn get homeworkId => text()();
TextColumn get title => text()();
DateTimeColumn get cachedAt => dateTime()();
// No primaryKey (use rowid)
}
// โ WRONG: JSON blob
class HomeworkDetailItems extends Table {
TextColumn get cacheKey => text()();
TextColumn get json => text()(); // JSON ๊ธ์ง
@override
Set<Column> get primaryKey => {cacheKey}; // primaryKey ์ค์ ๊ธ์ง
}
upsert prohibited -> DELETE + INSERT#
// โ
CORRECT: DELETE then INSERT
Future<void> saveItems(List<XxxCompanion> companions, String cacheKey) async {
await (delete(table)..where((t) => t.cacheKey.equals(cacheKey))).go();
await batch((b) => b.insertAll(table, companions));
}
// โ WRONG: upsert prohibited
await into(table).insertOnConflictUpdate(companion);
DB Migration: deleteTable + createTable#
onUpgrade: (Migrator migrator, int from, int to) async {
for (final table in allTables) {
await migrator.deleteTable(table.actualTableName);
await migrator.createTable(table);
}
},
DAO Rules#
- Handle Drift CRUD only (no domain entity dependency)
- Accept only Companion for INSERT
- Entity <-> Companion conversion is CacheRepository's responsibility
CacheRepository Conversion Methods#
// โ
CORRECT
static XxxEntity _toEntity(XxxItemData data) { ... }
static XxxCompanion _toCompanion(XxxEntity entity, String cacheKey) { ... }
// โ WRONG
static XxxEntity _dataToXxx(XxxItemData data) { ... }
UseCase Rules#
- All UseCases use Params class (when parameters exist)
- SWR UseCase: Does not implement UseCase interface,
_repositorygetter, returns Stream -
Future UseCase:
implements UseCase,repogetter, try-catch +Log.e - Unified OpenApiService (OpenApiClient prohibited)
Cache Invalidation After Write#
Delete related caches by prefix after POST/PUT/DELETE completion.
await dao.deleteByCacheKeyPrefix('schedule__get_memo');
await dao.deleteByCacheKeyPrefix('schedule__get_memos');
Related Documents#
- Client Cache Skill - Detailed implementation guide
- Caching Patterns Detail
- BLoC Patterns