Skip to main content

Walkthrough: Redis Cache

Sample: examples/redis_cache · Needs: Redis

A page-view counter backed by Redis. Every time a page is viewed, a counter goes up; you can also read the current count. The app is tiny, but it demonstrates a habit worth copying: the counting logic never mentions Redis, because Redis sits behind a small interface.

See the Redis package page for the full command surface.

Run it

Start Redis, then the app:

docker run -p 6379:6379 redis:7

cd examples/redis_cache
dart pub get
dart run bin/server.dart
curl -X POST localhost:8000/api/views/home   # {"page":"home","views":1}
curl -X POST localhost:8000/api/views/home # {"page":"home","views":2}
curl localhost:8000/api/views/home # {"page":"home","views":2}

Registering Redis

The RedisServiceProvider is added in config/app.dart with its connection settings:

// lib/config/app.dart
'providers': <ServiceProvider>[
RedisServiceProvider(
config: RedisConfig(
host: env('REDIS_HOST', 'localhost'),
port: env<int>('REDIS_PORT', 6379),
),
),
RouteServiceProvider(),
],

Once registered, Redis().command is available anywhere as a connection-pooled client.

The interface that keeps Redis out of the logic

The app depends on a three-method contract, not on Redis:

// lib/store/counter_store.dart
abstract interface class CounterStore {
Future<int> increment(String key);
Future<int> current(String key);
Future<void> reset(String key);
}

The real implementation maps those three methods onto Redis commands — INCR, GET, DEL:

// lib/store/redis_counter_store.dart
class RedisCounterStore implements CounterStore {
String _key(String key) => 'views:$key';

@override
Future<int> increment(String key) async =>
await Redis().command.incr(_key(key)) ?? 0;

@override
Future<int> current(String key) async {
final value = await Redis().command.get(_key(key));
return int.tryParse(value ?? '0') ?? 0;
}

@override
Future<void> reset(String key) async {
await Redis().command.del(_key(key));
}
}

INCR is the right tool here specifically because it is atomic: two simultaneous requests both increment correctly without a read-modify-write race. That is a Redis strength worth reaching for, not just a cache.

Note the views: prefix — namespacing your keys keeps this app's data from colliding with anything else sharing the Redis instance.

The service on top

PageViews is the application logic. It talks to a CounterStore and has no idea Redis exists:

// lib/views/page_views.dart
class PageViews {
PageViews(this._store);
final CounterStore _store;

Future<int> record(String page) => _store.increment(page);
Future<int> count(String page) => _store.current(page);
Future<void> reset(String page) => _store.reset(page);
}

The controller wires the real store into the service — the one place the choice of Redis is made:

// lib/features/views/views_controller.dart
final PageViews _pageViews = PageViews(RedisCounterStore());

class ViewsController extends Controller {
Future<Response> record(Request req, String page) async =>
Response.json({'page': page, 'views': await _pageViews.record(page)});

Future<Response> show(Request req, String page) async =>
Response.json({'page': page, 'views': await _pageViews.count(page)});
}

The routes

// lib/features/views/views_route.dart
Router.post('/views/{page}', viewsController.record);
Router.get('/views/{page}', viewsController.show);

Testing without Redis

Because PageViews depends on the CounterStore interface, the tests hand it an in-memory fake and never start Redis:

dart test

The counting logic is verified in full; the Redis wiring is a thin, boring adapter that does not need a test to prove INCR increments.

What to take away

  • Put your infrastructure (Redis, here) behind a small interface, and keep your logic on the interface side of it.
  • Redis is more than a cache — atomic operations like INCR solve concurrency problems cleanly.
  • Namespace your keys (views:home) so multiple apps can share one Redis safely.
  • The interface seam is what lets the logic be tested without the service running — the same pattern as Todos and Basic Authentication.