How to avoid stale data across multiple Riverpod family providers that fetch overlapping filtered subsets?
19:24 29 Jul 2026

Project Setup
I have a general architecture issue with my application. I'm using Riverpods 3 with Flutter for my frontend and a Python Fast API backend with Pydantic models.

My code is structured into packages with a requests package that holds all my API requests, a models package that holds the Freezed classes and a service layer provided by my riverpod providers. Then I have several consumers of these providers that query different subsets of my data from the backend.

Model:
Let's say I want to display a bunch of tasks in my application. I create a simple task model like this:

@freezed
abstract class Task with _$Task {
  factory Task({
    required String uuid,
    required TaskStatus status,
    required User lastEditor,
  }) = _Task;
}

Repository:
These tasks are retrieved from the backend with a GET /tasks endpoint inside the requests package and serialized using the fromJson provided by freezed.

static Future> getTasks(TaskFilters? filter) async {
  Map queryParams = filter?.toMap() ?? {};
  try {
    final response = await get(
      Uri.parse('$backendUrl/tasks').replace(queryParameters: queryParams),
      headers: await Requests.getAuthHeaders(),
    );
    if (Requests.isRequestSuccessful(response)) {
      final responseBody = utf8.decode(response.bodyBytes);
      List responseList = jsonDecode(responseBody);
      return responseList.map((json) => Task.fromJson(json)).toList();
    } else {
      throw Exception('Failed to get tasks');
    }
  } catch (e) {
    return Future.error(e);
  }
}

Providers:
I have a TaskProvider with a family property that filters by task status. It's a AsyncNotifier provider using code generation. There is a build function that gets the tasks with a set of query parameters, a function to add a new task and a function to change the status of a task.

static Future> getTasks(TaskFilters? filter) async {
  Map queryParams = filter?.toMap() ?? {};
  try {
    final response = await get(
      Uri.parse('$backendUrl/tasks').replace(queryParameters: queryParams),
      headers: await Requests.getAuthHeaders(),
    );
    if (Requests.isRequestSuccessful(response)) {
      final responseBody = utf8.decode(response.bodyBytes);
      List responseList = jsonDecode(responseBody);
      return responseList.map((json) => Task.fromJson(json)).toList();
    } else {
      throw Exception('Failed to get tasks');
    }
  } catch (e) {
    return Future.error(e);
  }
}

Say my database contains three tasks with different statuses:
Task A -> Planned
Task B -> In Progress
Task C -> Done

In my widgets now say I have a page that shows two columns. The columns show a simple List View of cards where each card has the task's uuid (A, B or C) and a chip with the task's status. The first column shows all the tasks in status [Planned, In Progress] and the second column shows tasks in status [In Progress, Done]. Notice there is some overlap because Task B will be shown in both column. To generate this data the first column with call ref.watch(tasksProvider(TaskFilters([TaskStatus.planned, TaskStatus.inProgress])) and the second column will call ref.watch(tasksProvider(TaskFilters([TaskStatus.inProgress, TaskStatus.done])) notably with a different family. So the first column will show List [A, B] and the second column will show List [B,C]

## Problems
I have two problems with this architecture:
Problem 1: Data gets stale between my providers when I change a task. Say that I change the status of task B from In Progress to Done. This should cause task B to disappear from the first column because it doesn't show done tasks. It should also update the status chip of the task in the second column because the task's status changed. However, what family should I pass to the notifier when I call patchTask? It needs to update in both providers but I only want to call the patch endpoint on the backend once. Alternatively I have to invalidate every instance of that provider and re-query all the data from the backend. This leads to a lot of wasted requests and constant building.
Potential Solution - Client side filtering: Ideally I would only keep one copy of each object in memory on the client's device. I would have a single tasks provider that stores every task that I could ever want and then sub-providers that listen to the top-level provider and get a subset of the tasks. Then the sub-task providers could be immutable and we only change the tasks in the family of the top level provider. However, if I were to do that I would need to do all the filtering locally on the client's device. This gets hard because in reality I am filtering by date (dealing with timezones), task assignees, status etc. It seems clunky to maintain an entire set of filters on the frontend instead of doing the filtering in the database.
Potential Solution - Backend UUID filtering: Alternatively to the above solution, I could maintain one top-level provider that stores every task the frontend has ever seen and then have smaller immutable providers with a family filter. The sub-providers could call a backend endpoint that returns a list of UUIDs that match a set of filters and then retrieve the relevant entries from the top level cache provider. However, this adds a ton of complication because I have to deal with cache time-to-live, cache misses etc. It also doesn't solve my main problem because I still don't know how to make sure those providers stay up to date when a new entry is added to the cache. That new entry may have to be added some some of the sub-providers and I don't know how to determine which ones without just invalidating all my providers.

Problem 2: Similar to problem 1, how do I deal with nested objects changing? Say that a user named Robert was the last person to edit task A. The database tracks the last user to modify the lastEditor field and returns a user object inside the task. That's fine until Robert changes their name to Bob. Now task A is showing the incorrect name of the user because it didn't know to re-query that specific task as a side-effect of renaming the user. Even if I had just stored the UUID of the user inside the task instead of the full object, I wouldn't know that the user's name changed because the UUID of the user will never change. I have no idea how to deal with this problem. It seems infeasible to track every single possible side-effect of renaming a user.

Any advice would be greatly appreciated. I don't know how to solve these systemic architecture issues and they are leading to many provider invalidations and stale data.

best-practices flutter riverpod