Skip to content

Datasource Module Pattern

The Datasource Module pattern provides a clean way to organize and register GraphQL request strategies and cache handlers, avoiding circular dependencies and maintaining separation of concerns.

The Datasource Module pattern solves the problem of strategy registration by:

  • Centralizing Strategy Registration: All strategies for a feature are registered in one place
  • Avoiding Circular Dependencies: Module creates datasource with strategies, not the other way around
  • Enabling Dependency Injection: Works seamlessly with Riverpod providers
  • Supporting Testing: Easy to mock and test individual strategies
abstract class DatasourceModule<T> {
T create(GqlClient client, RequestContext requestContext);
}

Enum-based keys for type-safe strategy registration:

enum HotelBookingsRequestStrategyKeys {
hotelBookingsList,
hotelBookingsCount,
hotelBookingsRequestedCount,
hotelBookingsCompletedCount,
hotelBookingsCanceledCount,
hotelBookingsAllCount,
hotelBookingsUpsert,
hotelBookingsDelete,
}
class HotelBookingsDatasourceModule
implements DatasourceModule<HotelBookingsDatasource> {
@override
HotelBookingsDatasource create(
GqlClient client,
RequestContext requestContext,
) {
final datasource = HotelBookingsDatasource(
client: client,
requestContext: requestContext,
);
// Register all request strategies
_registerRequestStrategies(requestContext);
// Register all cache handlers
_registerCacheHandlers(requestContext);
return datasource;
}
void _registerRequestStrategies(RequestContext requestContext) {
// List strategies
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsList.name,
HotelBookingsListRequestStrategy(),
);
// Count strategies with inheritance
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsCount.name,
HotelBookingsCountRequestStrategy(),
);
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsRequestedCount.name,
HotelBookingsRequestedCountRequestStrategy(),
);
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsCompletedCount.name,
HotelBookingsCompletedCountRequestStrategy(),
);
// Mutation strategies
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
HotelBookingsUpsertRequestStrategy(),
);
}
void _registerCacheHandlers(RequestContext requestContext) {
// Upsert cache handler (complex example)
requestContext.registerCacheHandler(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
HotelBookingsUpsertCacheHandlerStrategy(),
);
// Delete cache handler
requestContext.registerCacheHandler(
HotelBookingsRequestStrategyKeys.hotelBookingsDelete.name,
HotelBookingsDeleteCacheHandlerStrategy(),
);
}
}
void _registerBasicStrategies(RequestContext requestContext) {
requestContext.registerStrategy(
'strategyKey',
ConcreteStrategy(),
);
}

For strategies that share common behavior:

void _registerCountStrategies(RequestContext requestContext) {
// Base count strategy
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsCount.name,
HotelBookingsCountRequestStrategy(),
);
// Status-specific count strategies (inherit from base)
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsRequestedCount.name,
HotelBookingsRequestedCountRequestStrategy(),
);
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsCompletedCount.name,
HotelBookingsCompletedCountRequestStrategy(),
);
}

Register strategies based on configuration or feature flags:

void _registerConditionalStrategies(RequestContext requestContext) {
// Always register basic strategies
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsList.name,
HotelBookingsListRequestStrategy(),
);
// Conditionally register advanced strategies
if (FeatureFlags.advancedBookingEnabled) {
requestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsAdvanced.name,
HotelBookingsAdvancedRequestStrategy(),
);
}
}
void _registerSimpleCacheHandlers(RequestContext requestContext) {
requestContext.registerCacheHandler(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
HotelBookingsSimpleCacheHandlerStrategy(),
);
}

For handlers that manage multiple cache types:

void _registerComplexCacheHandlers(RequestContext requestContext) {
// Complex handler that updates multiple caches
requestContext.registerCacheHandler(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
HotelBookingsUpsertCacheHandlerStrategy(), // 1800+ lines
);
}
@riverpod
HotelBookingsDatasource hotelBookingsDatasource(
HotelBookingsDatasourceRef ref,
) {
final client = ref.watch(gqlClientProvider);
final requestContext = ref.watch(requestContextProvider);
return HotelBookingsDatasourceModule().create(client, requestContext);
}
@riverpod
IHotelBookingsRepository hotelBookingsRepository(
HotelBookingsRepositoryRef ref,
) {
final datasource = ref.watch(hotelBookingsDatasourceProvider);
return HotelBookingsRepository(datasource);
}
void main() {
group('HotelBookingsDatasourceModule', () {
late MockGqlClient mockClient;
late MockRequestContext mockRequestContext;
late HotelBookingsDatasourceModule module;
setUp(() {
mockClient = MockGqlClient();
mockRequestContext = MockRequestContext();
module = HotelBookingsDatasourceModule();
});
test('creates datasource with all strategies registered', () {
// Act
final datasource = module.create(mockClient, mockRequestContext);
// Assert
expect(datasource, isA<HotelBookingsDatasource>());
// Verify all strategies were registered
verify(() => mockRequestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsList.name,
any(),
)).called(1);
verify(() => mockRequestContext.registerStrategy(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
any(),
)).called(1);
});
test('registers cache handlers correctly', () {
// Act
module.create(mockClient, mockRequestContext);
// Assert
verify(() => mockRequestContext.registerCacheHandler(
HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name,
any(),
)).called(1);
});
});
}
void main() {
group('HotelBookingsDatasource Integration', () {
late ProviderContainer container;
setUp(() {
container = ProviderContainer(
overrides: [
gqlClientProvider.overrideWithValue(mockClient),
requestContextProvider.overrideWithValue(mockRequestContext),
],
);
});
test('datasource executes strategies correctly', () async {
// Arrange
final datasource = container.read(hotelBookingsDatasourceProvider);
// Act
final result = await datasource.queryList().first;
// Assert
expect(result.data, isNotNull);
});
});
}
  • Module handles registration logic
  • Datasource focuses on business operations
  • Strategies handle specific request/cache logic
  • Easy to mock individual strategies
  • Module can be tested independently
  • Clear dependency injection points
  • All strategies for a feature in one place
  • Easy to add/remove strategies
  • Clear registration patterns
  • Enum-based strategy keys prevent typos
  • Compile-time verification of strategy registration
  • Clear interfaces for all components

One module per feature (recommended):

HotelBookingsDatasourceModule
EventsDatasourceModule
UsersDatasourceModule

One module per operation type:

QueryDatasourceModule
MutationDatasourceModule
SubscriptionDatasourceModule

Combination of feature and operation:

HotelBookingsQueryModule
HotelBookingsMutationModule
  1. Use Enum Keys: Always use enum-based strategy keys for type safety
  2. Register All Strategies: Ensure all strategies are registered in the module
  3. Test Registration: Verify all strategies are properly registered
  4. Document Complex Handlers: Add comments for complex cache handlers
  5. Keep Modules Focused: One module per feature or logical grouping
  6. Use Consistent Naming: Follow naming conventions for strategies and handlers

The Datasource Module pattern provides a clean, testable, and maintainable way to organize GraphQL strategies while avoiding circular dependencies and supporting proper dependency injection.