Skip to content

Architecture Overview

MOFA Architecture Diagram

MOFA architecture is organized into four primary layers, each with clear responsibilities and defined sources of truth. This reference provides complete details for each layer.

Data flows through three model layers: GQL Raw Models -> Domain Models -> Presentation State

The Convertor pattern provides composable pipelines for transforming data across these layers.


Source of Truth:Remote data sources (APIs only)
  • Handles all remote data operations (GraphQL, REST APIs)
  • Implements repository patterns to abstract data access
  • Transforms GQL raw models into domain models via Convertor-based model convertors
  • Uses GraphQL with Ferry client and the Convertor pattern
  • No local storage - caching handled by dedicated service layer or Ferry’s built-in cache
  • GraphQL Clients: Ferry-based GraphQL operations
  • REST Clients: Dio-based HTTP operations
  • WebSocket Clients: Real-time data connections
  • Concrete classes implementing domain repository interfaces
  • Handle data fetching, cache coordination, and error handling
  • Transform GQL raw models to clean domain models using model convertors
  • Data cleaning happens here (null handling, default values, type mapping)
  • Request Convertors: Build Ferry operation requests from typed parameters
  • GraphQLRequestExecutor: Execute operations via Ferry client stream
  • GraphQLStreamConvertor: Unwrap Ferry response streams into clean data streams
  • CacheHandlerSpecs + UpdateCacheTypedLink: Declarative cache management
  • Model Convertors: Transform GQL raw types into domain models

Source of Truth:

Domain models, CRUD inputs, filters, sorts, domain exceptions

  • Defines core business entities as plain models (no business logic)
  • Contains domain models, value objects, CRUD inputs, filters, sorts
  • Provides interfaces for repositories
  • Domain-specific exceptions and validation
@freezed
class UserModel with _$UserModel {
const factory UserModel({
required String id,
required String name,
required String email,
String? avatar,
required DateTime createdAt,
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
@freezed
class CreateUserInput with _$CreateUserInput {
const factory CreateUserInput({
required String name,
required String email,
String? avatar,
}) = _CreateUserInput;
factory CreateUserInput.fromJson(Map<String, dynamic> json) =>
_$CreateUserInputFromJson(json);
}
@freezed
class UserFilter with _$UserFilter {
const factory UserFilter({
String? nameContains,
String? emailContains,
DateTime? createdAfter,
DateTime? createdBefore,
}) = _UserFilter;
}
enum UserSort { name, email, createdAt }
abstract class DomainException implements Exception {
const DomainException(this.message);
final String message;
}
class UserNotFoundException extends DomainException {
const UserNotFoundException(String userId)
: super('User with ID $userId not found');
}

Source of Truth:

Application-wide services, business logic, advanced use cases, cross-cutting concerns

Services are only required for complex logic. Simple CRUD flows go directly from Notifier to Repository. A service is required when:

  • Orchestrating multiple repositories in a single operation
  • Implementing cross-cutting concerns (error handling, analytics, permissions)
  • Coordinating complex business workflows (multi-step, rollback, state machines)
  • Managing caching strategies beyond simple read/write
  • Integrating external services (push notifications, file uploads)

See ADR-006: Service Layer Refinement for the full criteria.

  • Business logic: [Feature]Service (e.g., UserService)
  • Cross-cutting concerns: [Concern]OrchestratorService (e.g., DataMergeOrchestratorService)

Source of Truth:Presentation state and user interface
  • Manages presentation logic and user interface components
  • Uses notifiers (@riverpod codegen) to manage UI state and react to data changes
  • For simple CRUD, notifiers access repositories directly (no service needed)
  • For complex flows, notifiers delegate to services
  • Contains UI tests to ensure correct behavior

Every feature must handle these states:

  1. Loading: Show appropriate loading indicators
  2. Error: Display user-friendly error messages with retry option
  3. Empty: Show empty state with helpful guidance
  4. Success: Display the actual content

Data flows through three distinct model types:

  • Generated by Ferry via build.yaml custom type mapping
  • Mapped to Dart classes (not built_value) for direct use
  • Live in the datasource layer alongside GraphQL operations
  • Clean models defined with @freezed
  • Repository-processed: null handling, default values, type conversions
  • Live in the domain layer, consumed by services and UI
  • AsyncValue wrappers, pagination state, form state
  • Managed by notifiers in the UI layer
FromToWhoHow
GQL RawDomainRepositoryModel Convertor (.thenMap() / .thenEach())
DomainPresentationNotifierAsyncValue wrapping, state composition
Domain InputGQL Mutation VarsRepositoryFilter/Sort/Input Convertors

Houses core features that other features depend on:

  • Auth Feature: Authentication logic shared across features
  • Router: Centralized navigation logic and route definitions
  • Service Locators: @riverpod providers for dependency injection
  • Configuration: Optional flavor configuration and app-wide settings

Handles all caching scenarios:

  • Secure Cache: Sensitive data storage (tokens, credentials)
  • Simple Cache: Basic data caching (preferences, settings)
  • Complex Cache: Advanced scenarios (offline data, sync strategies)

Reusable, business-logic-independent functionality:

  • I18n Package: Internationalization and localization utilities
  • Assets Package: Asset management and resource handling
  • Convertor Package: Shared Convertor pattern implementation

Notifier -> Repository -> Datasource (Convertor pipeline)
Notifier -> Service -> Multiple Repositories -> Multiple Datasources
  • Caching Service: Integrates with Services layer for all storage needs
  • Core Folder: Provides shared features, service locators, and configuration to all layers
  • Generic Packages: Used by UI and Services layers as needed
  • UI directly calling Datasource
  • Datasource importing Services
  • Domain containing business logic
  • Local storage in Datasource layer
  • Feature-to-feature imports (use Core folder instead)
  • Passthrough services that add no logic (use direct Notifier -> Repository instead)

This architecture ensures maintainability, testability, and scalability while providing clear boundaries and responsibilities for each layer.