Pattern overview
The Service-Repository pattern splits the traditional “business logic layer” into two distinct layers:Service Layer
Orchestrates business logic, validates input, handles errors, and controls device hardware
Repository Layer
Abstracts data sources, manages Firebase queries, and returns domain models
Why this pattern?
This architecture solves several common Android development challenges:Separation of concerns
Separation of concerns
Services handle business logic and coordination, while Repositories focus solely on data access. This makes each component easier to understand and modify.
Testability
Testability
Services can be tested by mocking Repositories. Repositories can be tested independently of Firebase using test doubles.
Hardware abstraction
Hardware abstraction
Device-specific APIs (Speech, TTS, Location) are isolated in Services, keeping the UI layer framework-agnostic.
Error handling
Error handling
Services translate Firebase exceptions into domain-specific errors that the UI can present to users.
Reusability
Reusability
Repositories can be shared across multiple Services. Services can be used by different UI components.
Repository layer
Repositories are responsible for data access only. They know how to fetch and persist data, but contain no business logic.Key characteristics
All repository methods are
suspend functions that use Kotlin Coroutines for asynchronous operations.- Single data source per repository - Each repository manages one domain entity
- Returns domain models - Converts Firebase objects to domain models
- No error mapping - Throws exceptions as-is for Services to handle
- Framework-specific - Can use Firebase, Room, DataStore, or other Android APIs
Example: AuthRepository
Let’s examine the authentication repository:/home/daytona/workspace/source/app/src/main/java/com/demodogo/ev_sum_2/data/repositories/AuthRepository.kt
Key design decisions
Key design decisions
.await()extension - Converts FirebaseTaskto suspending functioncallbackFlow- Transforms Firebase listeners into Kotlin Flow- No error handling - Exceptions propagate to the Service layer
- Default parameters -
FirebaseAuth.getInstance()for easy testing with DI
Example: PhraseRepository
The phrase repository demonstrates CRUD operations with Firestore:/home/daytona/workspace/source/app/src/main/java/com/demodogo/ev_sum_2/data/repositories/PhraseRepository.kt
The repository automatically scopes phrases to the current user by using their Firebase Auth UID in the Firestore path.
- User scoping - Data is automatically isolated per user
- Timestamp ordering - Phrases are sorted by creation time (newest first)
- Domain model mapping - Firestore documents are converted to
Phraseobjects - Complete CRUD - Create, Read, Update, Delete operations
Example: LocationRepository
The location repository abstracts Google Play Services:/home/daytona/workspace/source/app/src/main/java/com/demodogo/ev_sum_2/data/repositories/LocationRepository.kt
Service layer
Services orchestrate business logic by coordinating between Repositories, validating input, handling errors, and controlling device hardware.Key characteristics
- Delegates to repositories - Services don’t access data directly
- Error mapping - Converts framework exceptions to user-friendly messages
- Hardware control - Manages Android APIs like Speech and TTS
- Business validation - Uses domain validators before calling repositories
- Coordination - Can call multiple repositories for complex operations
Example: AuthService
The authentication service adds error handling to repository calls:/home/daytona/workspace/source/app/src/main/java/com/demodogo/ev_sum_2/services/AuthService.kt
The Service layer catches exceptions from the Repository and maps them to user-friendly error messages. This keeps Firebase implementation details out of the UI.
Example: LocationService
This service demonstrates hardware abstraction and coordination:/home/daytona/workspace/source/app/src/main/java/com/demodogo/ev_sum_2/services/LocationService.kt
- Coordinates between
LocationRepositoryandGeocoder - Handles API level differences (Android 13+ uses callbacks)
- Converts exceptions to domain errors
- Returns a composite result with coordinates and address
Hardware controllers
Some services are pure hardware controllers with no repository:SpeechController
SpeechController
Wraps Android’s
SpeechRecognizer to provide voice input. Manages lifecycle and provides callbacks for partial and final results.TextToSpeechController
TextToSpeechController
Controls Android’s TTS engine for accessibility. Configured for Spanish (Chile) locale.
Pattern comparison
Here’s how the Service-Repository pattern compares to other approaches:- Service-Repository
- Direct Repository
- ViewModel + Repository
Pros:
- Clear separation between coordination and data access
- Services can coordinate multiple repositories
- Hardware abstraction is separated from data access
- Easy to test each layer independently
- More files and layers than simpler patterns
- Potential for thin Services that just delegate
Data flow through the pattern
Here’s a complete example of creating a phrase:Best practices
Follow these guidelines when implementing the pattern:Keep Repositories focused
Each repository should manage exactly one domain entity or data source
Services coordinate
Services can call multiple repositories but shouldn’t access data directly
Use domain models
Repositories return domain models, not Firebase or Room objects
Error mapping in Services
Repositories throw raw exceptions; Services map them to domain errors
Hardware in Services
Device APIs (Speech, Location, TTS) belong in the Service layer
Suspend everything async
Use
suspend functions and Flow for all asynchronous operationsTesting strategy
- Repository tests
- Service tests
- Domain tests
Test repositories with Firebase emulators or mock Firebase instances:
The app includes comprehensive unit tests for speech normalization in
SpeechNormalizersTest.kt that validate all phonetic combinations.Next steps
Project structure
Explore the complete directory structure and file organization