ttl_cache_service

pub package License: MIT

A high-performance in-memory cache for Dart & Flutter that keeps your expensive queries cached longer and lets you cache any async function with just .cache().


Why ttl_cache_service?

In traditional LRU caches, all cached data is treated equally:

  • A local config read takes ~1ms.
  • A heavy database query with joins takes ~1,200ms.

Under classic LRU, your expensive 1.2-second query gets kicked out of memory simply because a cheap 1ms config was read right after it.

ttl_cache_service solves this with Cost-Aware Eviction:

  1. Automatically tracks how long each query takes to fetch.
  2. When the cache is full, it evicts the cheapest queries first (the fastest to re-run), keeping your heaviest queries safely in memory.
  3. Falls back to LRU for ties.

Superpowers

  • ๐Ÿช„ Just add .cache(): Call .cache(...) directly on your functions. Keys are auto-generated.
  • โšก Latency-Aware Eviction: Prioritizes keeping high-latency queries in memory.
  • ๐Ÿ›ก๏ธ Stampede Protection: If 10 screens request the same data at the same millisecond, only 1 network call runs.
  • ๐Ÿงน Smart Invalidation: Invalidate by exact parameters, partial prefix, or function name.
  • ๐Ÿ“Š Quantifiable Latency Saved: See exactly how many milliseconds of network time your cache saved.
  • ๐Ÿš€ Zero Dependencies: 100% pure Dart. Works on Flutter (all platforms), Dart VM, Web, and Server.

Installation

dart pub add ttl_cache_service
# or, for Flutter projects:
flutter pub add ttl_cache_service

Quickstart

1. Zero-Boilerplate Function Caching (.cache())

Call .cache() directly on any asynchronous function (supports 0 to 4 arguments):

// 0 arguments
final profile = await api.getProfile.cache();

// 1 argument
final doctor = await api.getDoctor.cache(doctorId);

// 2 arguments
final appointments = await api.getAppointments.cache(doctorId, date);

// With optional settings:
final slots = await api.getSlots.cache(
  doctorId,
  date,
  ttl: const Duration(minutes: 10), // custom duration for this call
  forceRefresh: true,               // force re-fetch and update cache
);

2. Manual / Cache-Aside Pattern (getOrFetch)

If you prefer explicit cache management or need a custom cache instance:

import 'package:ttl_cache_service/ttl_cache_service.dart';

final cache = TtlCache<String, dynamic>(
  defaultTtl: const Duration(minutes: 5),
  maxEntries: 500,
  evictionPolicy: TtlEvictionPolicy.leastFetchTime,
);

// Fetches from network only if missing or expired:
final data = await cache.getOrFetch(
  'my_key',
  fetcher: () async => await remoteApi.fetchData(),
);

3. Smart Invalidation (.invalidateCache())

Invalidate directly on your function without typing manual string keys:

// Invalidate a specific appointment:
api.getAppointments.invalidateCache(doctorId, date);

// Invalidate all dates for a doctor (partial prefix match):
api.getAppointments.invalidateCache(doctorId);

// Invalidate all appointments across all doctors:
api.getAppointments.invalidateAllCache();

// Or invalidate on a TtlCache instance:
cache.invalidate('my_key');
cache.invalidatePrefix('doctor:123:');

Quick Reference: Function Caching Cheat Sheet

Your Function How to Cache How to Invalidate
getProfile() api.getProfile.cache() api.getProfile.invalidateCache()
getDoctor(id) api.getDoctor.cache(id) api.getDoctor.invalidateCache(id)
getAppointments(id, date) api.getAppointments.cache(id, date) api.getAppointments.invalidateCache(id, date)
getSlots(id, date, dept) api.getSlots.cache(id, date, dept) api.getSlots.invalidateCache(id, date, dept)
filter(a, b, c, d) api.filter.cache(a, b, c, d) api.filter.invalidateCache(...)

All .cache(...) calls accept optional settings: {Duration? ttl, bool forceRefresh, String? key, TtlCache? cache}.


Named Parameters

For functions with named parameters, use getOrFetch:

Future<List<Doctor>> searchDoctors({required String query, String specialty = 'All'}) async { ... }

final doctors = await TtlCache.defaultInstance.getOrFetch(
  'searchDoctors:$query:$specialty',
  fetcher: () => searchDoctors(query: query, specialty: specialty),
);

// Invalidate when needed:
TtlCache.defaultInstance.invalidate('searchDoctors:$query:$specialty');

Tip: You can also use TtlCache.buildKey(searchDoctors, [query, specialty]) to automatically generate deterministic keys without manual string concatenation.


Stampede / Thundering-Herd Protection

When 10 parallel requests call for the same key at the same moment, TtlCache executes the network fetch exactly once:

// All 10 callers share the same in-flight Future:
final results = await Future.wait(
  List.generate(10, (_) => api.getAppointments.cache(doctorId, date)),
);

Using with Flutter (Cubit example)

class DoctorSearchCubit extends Cubit<DoctorSearchState> {
  DoctorSearchCubit(this._search) : super(DoctorSearchInitial());

  final Future<List<Doctor>> Function(String, String) _search;

  Future<void> search(String query, String specialty) async {
    emit(DoctorSearchLoading());
    try {
      final doctors = await _search.cache(query, specialty);
      emit(DoctorSearchLoaded(doctors));
    } catch (e) {
      emit(DoctorSearchError(e.toString()));
    }
  }
}

Cache Stats & Saved Latency

print(TtlCache.defaultInstance.stats);
// Output:
// TtlCacheStats(hits: 42, misses: 3, hitRate: 93.3%, evictions: 0, size: 3, savedTime: 3200ms)

License

MIT License. See LICENSE for details.

Libraries

ttl_cache_service
High-performance in-memory TTL cache with latency-aware eviction (least fetch time evicted first), stampede protection, and func.getTtlCache() extensions.