Flutter Architecture: repository-service pattern

🕑 6 min read

Alt Text

The repository-service is not a new pattern, but it is powerful. I have personally used it for quite some time, and now I wanted to share it with you all.

In this post, we will see real examples of implementing the repository-pattern and also learn some pros and cons the pattern has.

Some background

To understand the repository-service pattern, we first need to understand what a repository and what a service is.

  • A repository is a class that will handle getting data into and out of your database (data store). For example, if you have a Dog model, you will probably also have a DogRepository.
  • A service is a class that will combine multiple repositories to provide more complex domain data. For example, if we have a Dog and a CatRepository, we might have an AnimalService to get all our animals and aggregate data.

You might already imagine a con of this pattern and, that is "boiler-plate" code, or, in simple terms, a bunch of god damn code 😡.

The practical application

Let's move away from the background and get into the coding!

To better explain this, we will keep the animals. I love animals, why not use them to explain some code!

Below we have two endpoints, one for dog and one for cat. I made them in singular, to better grasp the concept.

{
  "id": 1,
  "name": "Fluffy",
  "species": "cat",
  "age": 2,
  "breed": "persian"
}
{
  "id": 1,
  "name": "Bobby",
  "species": "dog",
  "age": 4,
  "breed": "Keeshound"
}

The repository in repository-service pattern

I will ignore the part where we create the models/entities for the above JSON, just imagine for now that we have it.

Now two typical repositories would look something like this

class DogRepository {
  Future<Dog> getDog() async {
    // Do the required code to fetch it
    // with whatever package or solution you want.
    try {
      final dog = await fancyFetchLogicForDog();
      return dog;
    } catch (e){
      throw CustomException(e);
    }
  }
}
class CatRepository {
  Future<Cat> getCat() async {
    // Do the required code to fetch it
    // with whatever package or solution you want.
    try {
    final cat = await fancyFetchLogicForCat();
    return cat;
    } catch (e){
      throw CustomException(e);
    }
  }
}

✅ Simplicity

Notice that both our repositories are very simple in terms of implementation. This is one of the reasons why I love repositories.

Now that we have done the above code, we just need to also create the services. Yes... that is right, one of the cons is now incoming...

We need to create 3 services:

  1. DogService
  2. CatService
  3. AnimalService

The reason for this is because depending on what we want in the application the layer we want to use the appropriate service.

Now how does these look?

🧙 The magic of services

I will keep it simple and ignore the DogService and CatService for now. The reason for this is because they will simply call respective repository and potential error handling, conversion, etc.

Instead let's focus on the AnimalService!

class AnimalService {
  final DogRepository _dogRepository;
  final CatRepository _catRepository;

  AnimalService(this._dogRepository, this._catRepository);

  Future<List<Animal>> getAllAnimals() async {
    try {
      final dog = await _dogRepository.getDog();
      final cat = await _catRepository.getCat();
      return [dog, cat];
    } on CustomException catch (e) {
      // Example will just rethrow the same exception.
      // I personally love using https://pub.dev/packages/multiple_result
      // and return a custom failure.
      rethrow;
    }
  }

  Future<double> getAverageAnimalAge() async {
    try {
      final dog = await _dogRepository.getDog();
      final cat = await _catRepository.getCat();
      final animals = [dog, cat];
      final averageAge = animals.map((animal) => animal.age).reduce((a, b) => a + b) / animals.length;
      return averageAge;
    } on CustomException catch (e) {
      // Example will just rethrow the same exception.
      // I personally love using https://pub.dev/packages/multiple_result
      // and return a custom failure.
      rethrow;
    }
  }
}

On purpose, I didn't do any kind of error handling to keep the code to a minimum. But if you would like to see that, make sure to share the article. This is something I cover in-depth in my course, but it requires a quite large blog post! For something free, I made a personal project open-source that implements this pattern.

Get my free Flutter Tips PDF

Level up your skills by joining 1600+ other Flutter developers!

The end result

Thanks to this pattern we have gotten a clean way to interact with our backend data.

// Imagine this has some fancy ui and this would be the method call.
Future<void> getAverageAnimalAge() async {
  // The service would do all the heavy lifting of converting the data
  // to the required response. If you used https://pub.dev/packages/multiple_result
  // you can here, handle that failure.
  final averageAnimalAge = await animalService.getAverageAnimalAge();

  setState(() {
    _averageAnimalAge = averageAnimalAge;
  });
}

But what I love the most is the freedom to do our error handling just as we want and still keep the calling side clean.

Some notes from feedback

There are some confusion for people with Android background. I want to personally quote something that I wrote about in this github issue.

"I personally follow DDD: View - Controller - Application Service - Repository

To me, this has made the most sense and has made my code the most decoupled. The reason I've gone with this is in my experience a lot of people coming from other technologies has a base understanding that a Repository handles crud operations while Service handles the business request. I.e I need x from x repository but also y from y repository, put these together and we have that value xy in the service.

The controller would then take the combined value (xy) from the service.

Now there are also times when a Repository is not needed or a Service is not needed but would only clutter. But I think we should be careful when going for the Android style as there are not only people coming from an Android background and to them a Repository calling a Service does not make sense to the definitions of the words. But to others this make a complete sense.

I just want to add that I don't think any of the approaches are bad but just wanted to give an additional opinion 😊"

You can, of course, have another "DataStore" layer that the repository have access too!

Ending gratitude

I would like to both shout-out Andrea Bizotto from with his astounding article on the repository pattern

And Matthew Jones about his service-repository article that I highly recommend reading through.