Flutter Architecture: The Repository-Service Pattern


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 defined as.

You might already imagine a con of this pattern and, that is “boiler-plate” 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 like animals :).

Below we have fake responses and two endpoints, one for dog and one for cat.

{
  "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 like repositories.

Now that we have done the above code, we also need to create the services. Here is where some of the boiler-plate jumps in.

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, but you could use a
      // functional approach if wanted.
      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, but you could use a
      // functional approach if wanted.
      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. Error handling is a blog post in of itself, but I have small open-source project if you want to see some examples that implements this pattern.

The end result

Thanks to this pattern we have gotten a predictable 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 functional error handling 
  // you can here, handle that failure. Or use `try-catch`.
  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. It’s up to you.

Ending gratitude

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

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