ADR-006: Service Layer Refinement
Service Layer Refinement
Services are reserved for complex business logic. Simple CRUD flows go directly from Notifier to Repository.
1. History (The Problem Context)
Section titled “1. History (The Problem Context)”The Problem
Section titled “The Problem”The original four-layer architecture (see ADR-001) positioned the service layer as mandatory for all data flows. In practice, this created unnecessary indirection:
- Passthrough services: Many services simply forwarded calls from notifiers to repositories without adding logic
- Artificial complexity: Simple list/detail features required a service class that added no value
- Slower development: Every feature required creating a service even when not needed
- Misleading architecture: The presence of a service implied business logic existed when it didn’t
Context
Section titled “Context”- When: After implementing 15+ features following the mandatory service layer rule
- Where: All Flutter projects using MOFA architecture
- Who: Flutter development team
- Constraints:
- Must maintain clear separation of concerns
- Must have a clear rule for when services are required vs optional
- Must not compromise testability
- Must support both simple CRUD and complex orchestration workflows
2. Reason (Architecture Decision Record)
Section titled “2. Reason (Architecture Decision Record)”Decision
Section titled “Decision”The service layer is optional for simple CRUD flows and required for complex logic. The criteria for “complex” are explicitly defined.
When a Service IS Required
Section titled “When a Service IS Required”A service is required when the logic involves any of these criteria:
- Multi-repository orchestration - Coordinating data from 2+ repositories in a single operation
- Cross-cutting concerns - Error handling orchestration, analytics tracking, permission checks that span features
- Complex business workflows - Multi-step operations with rollback, approval flows, state machines
- Data transformation beyond mapping - Computed fields, aggregations, business rule application
- Caching strategy coordination - Complex cache invalidation, TTL management, offline queue management
- External service integration - Third-party API calls, push notification orchestration, file upload coordination
When a Service is NOT Required
Section titled “When a Service is NOT Required”A service is not needed when:
- Simple CRUD - Fetching a list, getting details, creating/updating/deleting a single entity
- Direct repository access - The notifier needs data from exactly one repository with no transformation
- UI-only logic - Pagination state, form validation, loading states (these belong in notifiers)
Data Flow Comparison
Section titled “Data Flow Comparison”Simple CRUD flow (no service):
Notifier -> Repository -> DatasourceComplex flow (with service):
Notifier -> Service -> Multiple Repositories -> Multiple DatasourcesExamples
Section titled “Examples”No Service Needed
Section titled “No Service Needed”// Simple list fetch - notifier accesses repository directly@riverpodclass AttendeeListNotifier extends _$AttendeeListNotifier { @override Stream<List<AttendeeModel>> build() { final repository = ref.watch(attendeeRepositoryProvider); return repository.queryList( filter: AttendeeFilterCriteria(eventId: eventId), sort: AttendeeSortCriteria(name: SortOptions.ascending), ); }}Service Required
Section titled “Service Required”// Complex orchestration - service coordinates multiple repos@riverpodclass BookingService extends _$BookingService { Future<BookingResult> createBooking(BookingInput input) async { final bookingRepo = ref.read(bookingRepositoryProvider); final availabilityRepo = ref.read(availabilityRepositoryProvider); final notificationService = ref.read(notificationServiceProvider);
// 1. Check availability across multiple resources final isAvailable = await availabilityRepo.checkSlot( input.resourceId, input.startTime, input.endTime);
if (!isAvailable) { throw BookingConflictException(input.resourceId); }
// 2. Create the booking final booking = await bookingRepo.create(input);
// 3. Send confirmation notification await notificationService.sendBookingConfirmation(booking);
// 4. Update availability cache await availabilityRepo.invalidateSlot(input.resourceId);
return booking; }}Alternatives Considered
Section titled “Alternatives Considered”-
Keep Service Layer Mandatory
- Pros: Consistent architecture, every feature has the same shape
- Cons: Passthrough services add noise, slower development for simple features
-
Remove Service Layer Entirely
- Pros: Simplest possible architecture
- Cons: Complex orchestration logic would leak into notifiers
-
Optional Service Layer with Clear Criteria (Chosen)
- Pros: Right level of abstraction per feature, clear decision criteria
- Cons: Developers must evaluate criteria (mitigated by explicit checklist)
Service Naming Conventions
Section titled “Service Naming Conventions”| Type | Naming | Example |
|---|---|---|
| Feature business logic | [Feature]Service | BookingService |
| Cross-cutting orchestration | [Concern]OrchestratorService | DataMergeOrchestratorService |
| External integration | [Integration]Service | PushNotificationService |
3. Impact & Consequences
Section titled “3. Impact & Consequences”Positive Consequences
Section titled “Positive Consequences”- Faster development for simple features (skip unnecessary service layer)
- Clearer architecture - service presence signals actual business logic
- Less code to maintain - no passthrough services
- Better onboarding - new developers understand when to create services
Negative Consequences
Section titled “Negative Consequences”- Judgment required - developers must evaluate criteria (mitigated by the explicit checklist above)
- Refactoring risk - a simple feature may need a service later (mitigated: adding a service later is straightforward)
Decision Checklist
Section titled “Decision Checklist”When starting a new feature, use this checklist:
- Does this feature need data from 2+ repositories? -> Service required
- Does this feature have multi-step business logic? -> Service required
- Does this feature need cross-cutting concern orchestration? -> Service required
- Is this a simple CRUD operation on a single entity? -> No service needed
- Is this a list/detail view with filtering and sorting? -> No service needed