Hello! In our last lesson, we built the core business logic for our Number Trivia feature by defining the NumberTrivia entity and the GetConcreteNumberTrivia and GetRandomNumberTrivia use cases. You may recall that our use cases declared a dependency on a NumberTriviaRepository class, even though we hadn't created it yet.
Today, we will bridge that gap by formally defining the contract for that repository. The learning outcome for this lesson is to create repository interfaces (abstract classes) in the domain layer. This step is crucial for enforcing the Dependency Rule and creating a clean separation between your app's business logic and its data-sourcing mechanisms.
1. The Repository as a Contract
In Clean Architecture, the domain layer should not depend on the data layer. Instead, the data layer should depend on the domain layer. This is achieved through the Dependency Inversion Principle.
We don't want our use cases to know anything about http, dio, Firebase, or SQLite. They only need to know that there is something that can fetch the data they need. We define this "something" as a repository interface, which in Dart is an abstract class.
This abstract class acts as a contract. It dictates the methods that any data-fetching class must implement to be considered a valid repository for our domain. The domain layer owns this contract, and the data layer must adhere to it.

Let's dive into the theory behind this powerful concept.
Flutter TDD Clean Architecture Course [1] – Explanation & Project Structure
This video from Reso Coder's acclaimed series on Clean Architecture explains the role of the repository interface and the concept of dependency inversion.
Watch the segment from the domain layer. As you watch, focus on: The idea that the domain layer should be independent of "the whims of changing data sources." How the repository is described as being on the "edge" between the domain and data layers. The explanation of dependency inversion as creating an abstract class that defines a contract of what the repository must do.
As the video explains, the domain layer doesn't care how the trivia is fetched (from a remote API, a local cache, etc.), only that it can be fetched according to the methods defined in the contract.
2. Defining the NumberTriviaRepository Interface
Now that we understand the "why," let's implement the "how." We'll create the NumberTriviaRepository abstract class that our use cases from the previous lesson depend on.
First, let's think about what methods this contract needs. Looking back at our use cases:
GetConcreteNumberTrivianeeds to fetch trivia for a specific number. Itscallmethod expects to invoke something likerepository.getConcreteNumberTrivia(params.number).GetRandomNumberTrivianeeds to fetch trivia for a random number. Itscallmethod expects to invokerepository.getRandomNumberTrivia().
Both methods must handle potential failures (like no internet connection) and successes. Therefore, they should both return a Future<Either<Failure, NumberTrivia>>, just like our use cases.
Based on this, we can define our abstract class. Inside the lib/features/number_trivia/domain/ directory, create a new subfolder named repositories. Inside it, create a file named number_trivia_repository.dart.
// lib/features/number_trivia/domain/repositories/number_trivia_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../entities/number_trivia.dart';
abstract class NumberTriviaRepository {
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}
That's it. This file is the complete contract. It's concise, clear, and fully defines the data access requirements for our feature's domain logic. Any class that wishes to serve as the repository for this feature must provide concrete implementations for these two methods.
To see this pattern applied in a slightly different context, the following resource provides another excellent example.
Unlocking Clean Architecture in Flutter: A Practical Guide
This article from Raúl Ferrer provides a clear, practical guide to Clean Architecture. The section on repositories in the domain layer perfectly mirrors what we've just done.
Read the subsection titled "Repositories" within the section "1. Define your domain layer". Notice how the TaskRepository abstract class defines the methods addTask and getTasks, establishing the contract for a to-do app feature.
3. The Domain Layer: Complete
With the creation of the repository interface, the domain layer for our NumberTrivia feature is now functionally complete. It contains:
- Entities (
NumberTrivia): The business objects. - Use Cases (
Get...Trivia): The business rules that operate on those objects. - Repository Interface (
NumberTriviaRepository): The contract that defines how data for those objects should be accessed.
This layer is now a self-contained, high-level representation of our feature. It is pure Dart, has no dependencies on Flutter or any data-sourcing packages, and is therefore highly portable and testable. Your CS background will appreciate the elegance of this design—it achieves high cohesion and low coupling by strictly enforcing boundaries.
The article below provides another perspective, showing the file structure and giving a barebones code example that reinforces this separation.
Journey to the clean architecture for my flutter app - DEV Community
This DEV Community post by Lee Hae-seong shows a practical journey of refactoring an app to Clean Architecture.
In the section "Layer-First structure", look at the folder layout to see where a_repository.dart (the interface) and a_repository_impl.dart (the implementation) live. Then, scroll down to the code snippet under the heading "Interfacing between domain and data layer". This snippet shows an abstract class RepositoryA which is another perfect example of the repository contract we've built today.
Conclusion
In this lesson, you have defined the critical boundary between your application's domain and data layers. By creating an abstract repository class, you have established a formal contract that isolates your business logic from implementation details.
Key Takeaways:
- Repository interfaces are defined as
abstract classes within the domain layer. - They act as a contract, specifying what data operations the application needs, but not how they are performed.
- This approach is a practical application of the Dependency Inversion Principle, ensuring the data layer depends on the domain layer, not the other way around.
- The methods in the repository interface are derived from the needs of the use cases.
In our previous lessons, we built the domain logic. Today, we defined the contract for how that logic gets its data. In our next lesson, we will finally cross the boundary into the data layer to implement this repository interface, connecting it to a remote data source to fetch real data from a network API.