ADR-005: Convertor Replaces Strategy Pattern
Convertor Replaces Strategy Pattern
The Convertor pattern is now the sole datasource pattern in MOFA architecture. The Strategy/DatasourceModule pattern is retired.
1. History (The Problem Context)
Section titled “1. History (The Problem Context)”The Problem
Section titled “The Problem”The Strategy/DatasourceModule pattern, while solving Ferry’s circular dependency problem (see ADR-003), introduced significant boilerplate and complexity:
- Excessive class proliferation: Each feature required
RequestStrategy,CacheHandlerStrategy, strategy key enums,RequestContext, andDatasourceModuleclasses - Manual registration: Strategies had to be manually registered in the module, creating opportunities for runtime errors when registrations were missed
- Rigid composition: The pattern didn’t compose well - combining strategies or reusing parts across features required additional abstraction
- Testing overhead: Mocking the strategy registration chain was complex and brittle
- Conversion extension problem: Manual mapping between GQL types and domain models via extensions led to forgotten fields discovered only at runtime
Context
Section titled “Context”- When: After implementing the Strategy pattern across multiple production features
- Where: All Flutter projects using MOFA architecture with GraphQL
- Who: Flutter architecture team
- Constraints:
- Must still solve Ferry’s circular dependency problem
- Must integrate with Riverpod dependency injection
- Must support complex cache operations (optimistic updates, cache merging)
- Should reduce boilerplate while maintaining type safety
2. Reason (Architecture Decision Record)
Section titled “2. Reason (Architecture Decision Record)”Decision
Section titled “Decision”Replace the Strategy/DatasourceModule pattern with the Convertor pattern as the sole datasource abstraction in MOFA architecture. The Convertor pattern is implemented as a shared Dart package reusable across all projects.
What Was Removed
Section titled “What Was Removed”The following concepts from the Strategy pattern are retired:
| Retired Concept | Convertor Replacement |
|---|---|
RequestStrategy | Convertor<OperationRequest, Params> |
CacheHandlerStrategy | CacheHandlerSpecs + UpdateCacheTypedLink |
RequestContext | Direct Convertor composition |
DatasourceModule | @riverpod provider functions |
GqlDatasource abstract class | GraphQLRequestExecutor Convertor |
| Strategy key enums | Not needed (no registration) |
| Manual strategy registration | Not needed (direct composition) |
What Was Added
Section titled “What Was Added”| New Concept | Purpose |
|---|---|
Convertor<To, From> | Core unit: accepts input, produces output |
StreamConvertor<To, From> | Convertor producing Stream<To> |
AsyncConvertor<To, From> | Convertor producing Future<To> |
Chain | Sequence of Convertors composed together |
Interceptor | Side effects without modifying data flow |
Decorator | Modify input/output or bypass Convertor |
GraphQLRequestExecutor | Executes Ferry operations |
GraphQLStreamConvertor | Unwraps Ferry response streams |
CacheHandlerSpecs | Declarative cache operation configuration |
UpdateCacheTypedLink | Ferry TypedLink that applies CacheHandlerSpecs |
Alternatives Considered
Section titled “Alternatives Considered”-
Improve Strategy Pattern
- Add code generation for strategy registration
- Pros: Minimal migration effort
- Cons: Doesn’t address fundamental complexity; still too many classes
-
Repository-Only Pattern
- Move all logic into repositories, remove datasource abstraction
- Pros: Simple
- Cons: Loses composability; repositories become bloated; no reuse across features
-
Convertor Pattern (Chosen)
- Functional, composable pipelines
- Pros: Minimal boilerplate, highly composable, testable, type-safe
- Cons: Requires learning functional composition
Rationale
Section titled “Rationale”The Convertor pattern was chosen because:
- Solves the same problems as Strategy (circular dependencies, type safety, cache management) with far less code
- Composability - Convertors chain naturally:
executor.then(streamConvertor).map(extract).thenEach(modelConvertor) - No registration - No module pattern needed; Convertors are composed directly in
@riverpodproviders - Declarative cache -
CacheHandlerSpecsreplaces imperative cache handler registration with clear intent - Conversion extension problem solved - Ferry
build.yamlcustom type mapping generates Dart classes directly for GQL types, eliminating manual conversion extensions that led to forgotten fields - Shared package - Convertor is a standalone Dart package, reusable across all MOFA projects
3. Migration Guide
Section titled “3. Migration Guide”Before (Strategy Pattern)
Section titled “Before (Strategy Pattern)”// 1. Strategy key enumenum ChatRequestStrategyKeys { chatList, chatUpsert }
// 2. Request strategy classclass ChatListRequestStrategy implements ListItemsRequestStrategy<GQueryChatReq, GchatFilters, GchatOrder> { @override final String baseRequestId = 'chat'; @override GQueryChatReq createRequest(ListRequestParams params) { /* ... */ } // ... more boilerplate}
// 3. Cache handler strategy classclass ChatUpsertCacheHandlerStrategy implements CacheHandlerStrategy<GMutateChatSaveData, GMutateChatSaveVars> { @override UpdateCacheHandler build(RequestContext requestContext) { /* ... */ }}
// 4. Datasource moduleclass ChatDatasourceModule extends DatasourceModule<ChatDatasource> { @override ChatDatasource create(GqlClient client) { final requestContext = RequestContext(); registerStrategies(requestContext); registerCacheHandlers(/* ... */); return ChatDatasource(client: client, requestContext: requestContext); } @override void registerStrategies(RequestContext context) { context.registerStrategy(ChatListRequestStrategy()); context.registerStrategy(ChatUpsertRequestStrategy()); } // ...}
// 5. Riverpod provider@riverpodChatDatasource chatDatasource(ChatDatasourceRef ref) { return ChatDatasourceModule().create(ref.watch(gqlClientProvider));}After (Convertor Pattern)
Section titled “After (Convertor Pattern)”// 1. Request convertor (replaces strategy class + key enum)@riverpodConvertor<GQueryChatReq, ListRequestParams<GchatFilters, GchatOrder>>chatListQueryConvertor(Ref ref) { return Convertor((params) { return GQueryChatReq((b) => b ..requestId = 'chat_list_${params.toString()}' ..vars.G_filter = params.filter?.toBuilder() ..vars.G_sort = params.sort?.toBuilder() ..vars.G_skip = params.skip ..vars.G_take = params.take); });}
// 2. Datasource pipeline (replaces datasource module + abstract datasource)@riverpodStreamConvertor<List<GQueryChatData_chat_chatItems>, ListRequestParams<GchatFilters, GchatOrder>>chatListDatasource(Ref ref) { return GraphQLRequestExecutor<GQueryChatData, ListRequestParams<GchatFilters, GchatOrder>, GQueryChatVars>( gqlClient: ref.watch(gqlClientProvider), convertor: ref.watch(chatListQueryConvertorProvider), ) .then(GraphQLStreamConvertor()) .map((data) => data.chat!.chatItems!.nonNulls.toList());}
// 3. Cache specs (replaces cache handler strategy class)// Defined declaratively in the UpdateCacheTypedLink configurationKey Migration Notes
Section titled “Key Migration Notes”- No more strategy key enums - Convertors are composed directly, no registration
- No more DatasourceModule -
@riverpodproviders replace module factories - No more abstract GqlDatasource -
GraphQLRequestExecutoris the standard executor - Cache handling is declarative - Use
CacheHandlerSpecswithUpdateCacheTypedLink - Model conversion stays in repository - Use
.thenMap()/.thenEach()with model convertors
4. Impact & Consequences
Section titled “4. Impact & Consequences”Positive Consequences
Section titled “Positive Consequences”- ~60% reduction in datasource code per feature
- Eliminated forgotten strategy registration bugs (no registration needed)
- Composable building blocks reusable across features
- Simpler testing - test Convertors as pure functions
- Solved conversion extension problem via Ferry custom type mapping
Negative Consequences
Section titled “Negative Consequences”- Migration effort for existing features using Strategy pattern
- Learning curve for functional composition paradigm
- Two patterns in codebase during migration period (mitigated by completing migration before shipping)
Success Metrics
Section titled “Success Metrics”- All new features use Convertor pattern exclusively
- No references to
DatasourceModule,RequestStrategy, orCacheHandlerStrategyin active code - Measurable reduction in lines of code per feature datasource