If you already use Google Cloud Firestore, you probably know its data model and you noticed that Subcollections are one of the most interesting features.
I met them for the first time while working on my personal pet-project (a project where I can experiment with new technologies and methodologies). 
So, I wondered if there was a way to adapt the Repository Pattern to deal with Firestore Subcollections.
The main aspect to address was that in the Repository Pattern each repository is usually related to a specific model, in other words, it is related to a type. But reading from Google documentation page:
A Subcollection is a collection associated with a specific document.
This means that, for a subcollection, a repository is not related to a type but to a specific Document.
Let's see this experiment results... 🔬
Scenario
To get the point imagine this practical example:
We have to deal with "Houses" and each of these have some "Rooms" (a bathroom, a kitchen, ...). In each room there is the "Furniture" (a bed, two chairs and so on).
To model this scenario we can imagine to implement these entities:
- Furniture Item
 - Room
 - House
 
and they will be related in this way:
Houses   <--- a collection
 |
 |--- My-House <---a Document of Houses
        |--Address <---- a field of "My-House" document
        |--Rooms   <----- a Subcollection of "My-House"
            |
            |--Kitchen
            |--Bathroom
            |--Bedroom <--- a document of Rooms
                    |
                    |--FurnitureItems  <--- a Subcollection of Bedroom document
                            |
                            |-- Bed
                            |-- Wardrobe
The Repository
For the sake of simplicity let's focus on these two methods
- "Add" adds a document to the collection
 - "GetById" returns a document by its Id
 
Speaking in terms of interface we have:
public interface IRepository<T>{
    Either<String,String> Add(T data);
    Either<String, T> GetById(String id);
}
Ignoring for a moment Subcollections, an implementing class could be:
public  class  FirestoreRepository<T> implements IRepository<T>{
    private CollectionReference collection;
    private Class<T> classType;
    public FirestoreRepository(Firestore firestore, String collectionPath, Class<T> classType){
        this.collection = firestore.collection(collectionPath);
        this.classType = classType;
    }
    public Either<String,String> Save(T data){ 
        try{
            ApiFuture<DocumentSnapshot> futureResult = collection.add(data).get().get();
            DocumentSnapshot result = futureResult.get();
            return Either.right(result.getId());
        }catch(Exception ex){
            return Either.left(ex.getMessage());
        }
    }
    public Either<String, T> GetById(String id) {
        try{
            ApiFuture<DocumentSnapshot> futureEntry = collection.document(id).get();
            DocumentSnapshot entry = futureEntry.get();
            return entry.exists()
                ? Either.right(entry.toObject(classType))
                : Either.left("Data not found for id: "+id);
        }catch(Exception ex){
            return Either.left(ex.getMessage());
        }
    }
}
Note:
The GoogleApi methods to retrieve a DocumentSnasphot are async, they indeed returns an ApiFuture (a GoogleAPI implementation of java Future).
For the sake of simplicity, I chose to avoid the async approach blocking the execution until the future is complete. 
Yes, I know, It's not a good idea to do in real projects 🤓
The above implementation assumes that all the collection are at root level, in the constructor the collection reference is indeed created from the firestore object:
this.collection = firestore.collection(collectionPath);
This assumption is not correct when we use the firestore Subcollections. A Subcollection is related to a specific document, so we need
a DocumentReference to get one of its collections.
For example:
CollectionReference furnitureCollection = firestore
        .collection("Houses")
        .document("My-House")
        .collection("Rooms")
        .document("bathroom")
        .collection("FurnitureItems");
We notice that we need to chain CollectionReferences and DocumentReference to access to the right Subcollection.
So, as first, we need to modify the constructor of our FirestoreRepository to accept a CollectionReference.
public FirestoreRepository(CollectionReference collection, Class<T> classType){
        this.collection = collection;
        this.classType = classType;
    }
In this way we have removed the assumption that each collection is at root level.
The Repository Factory
At this point let me introduce the RepositoryFactory whose responsibility is:
- chain CollectionReferences and DocuementReferences
 - create an IRepository instance for a specific Collection (or Subcollection).
 
For example, if we want to get the Repository for the "FurnitureItems"
of the document "bathroom" relative to the "My-House", we expect to have something like:
  IRepository<FurnitureItem> furnitureRepo = repoFactory
        .FromDocument(House.class, "My-House")
        .FromDocument(Room.class, "bathroom")
        .GetRepository(FurnitureItem.class);
    furnitureRepo.Add(new FurnitureItem("chair"));
Otherwise, if we want to query through the House collection we should have:
  IRepository<House> houseRepo = repoFactory.GetRepository(FurnitureItem.class);
  houseRepo.Add(new House());
We notice that the method FromDocument should return a new instance of RepositoryFactory. This allows us to keep a RepositoryFactory reference to access all its Subcollections.
For example consider that a "House", in addition to the "Rooms" collection, has a "Tenants" collection. In this scenario we can write:
RepositoryFactory myHouseRepoFactory = repoFactory.FromDocument(House.class, "MyHouse");
myHouseRepoFactory
    .FromDocument(Room.class, "kitchen") //this do not modify the collection root of myHouseRepoFactory
    .GetRepository(FurnitureItem.class)
    .Add(new FurnitureItem());
myHouseRepoFactory
    .GetRepository(Tenant.class)
    .Add(new Tenant());
Design the RepositoryFactory 📐 ✏️
Bearing in mind everything, our RepositoryFactory needs:
- A map to link the Model classType with a collection name
 - A pointer to create a repository: if the repo is at root level then it should be a firestore instance, a DocumentReference otherwise.
 - A method to move the pointer to the next DocumentReference returning a new RepositoryFactory instance.
 - A method to get the IRepository instance for a specified collection (or Subcollection)
 
The Model-Collection Map
To link each model class type with the relative collection name we use a map like this:
private final static Map<Class<?>, String> collectionMap = Map.of(
        House.class, "Houses",
        Room.class, "Rooms",
        FurnitureItem.class, "Furniture"
    );   
The Pointer
Keep in mind that we have two kinds of pointer:
- The Firestore instance if the collection is at the root level
 - A DocumentReference if the collection is relative to a specific Document (a.k.a Subcollection)
 
We could keep a reference to Firestore and one to a DocumentReference, then we should have some logic to choose what of them to use.
public class RepositoryFactory{
    ....
    private Firestore firestore;
    private DocumentRefernce documentReference;
    private Boolean isRootLevel;
    public RepositoryFactory(Firestore firestore){
        this.firestore = firestore;
        isRootLevel = true;
    }
    private RepositoryFactory(DocumentReference documentReference){
        this.documentReference = documentReference;
        isRootLevel = false;
    }
    ....
    private CollectionReference getCollectionReference(String collectionName){
        return isRootLevel
            ? firestore.collection(collectionName)
            : documentReference.collection(collectionName);
    }
    ....
}
But when move the pointer from the root level to a document level, the firestore reference is no more useful.
Is there a way to keep only the pointer we need?
💡 Functional approach to the rescue!
We could indeed keep only a reference to a function that, given a collection name, retrieves a CollectionReference:
public class RepositoryFactory{
    ....
    private Function<String, CollectionReference> getCollectionReference;
    ....
    public RepositoryFactory(Firestore firestore){
        getCollectionReference = collectionPath -> firestore.collection(collectionPath);
    }
    private RepositoryFactory(DocumentReference document){
        getCollectionReference = collectionPath -> document.collection(collectionPath);
    }
    ....
}
In this way, each instance of RepositoryFactory keeps only the reference to the right pointer.
Move the pointer to a specific Document
To move through DocumentsReference(s) we don't want to modify the existing RepositoryFactory instance but we have to create a new one with the new DocumentReference as pointer:
 public  RepositoryFactory FromDocument(Class<?> classType, String documentId){
        String collectionName = collectionMap.get(classType);
        DocumentReference documentReference = getCollectionReference.apply(collectionName).document(documentId);
        return new RepositoryFactory(documentReference);
    }
Create a Repository for a given Model
At this point we need a method that, given a Model classType, returns a IRepository<> instance:
 public <T> IRepository<T> GetRepository(Class<T> classType){
        String collectionName = collectionMap.get(classType);
        return new FirestoreRepository<T>(getCollectionReference.apply(collectionName), classType);
    }
Pick up the pieces
Finally, our Repository Pattern implementation should be something like this:
//IRepository.java
public interface IRepository<T>{
    Either<String,String> Add(T data);
    Either<String, T> GetById(String id);
}
//FirestoreRepository.java
public  class  FirestoreRepository<T> implements IRepository<T>{
    private CollectionReference collection;
    private Class<T> classType;
    public FirestoreRepository(CollectionReference collection, Class<T> classType){
        this.collection = collection;
        this.classType = classType;
    }
    public Either<String,String> Add(T data){ 
        try{
            DocumentSnapshot result = collection.add(data).get().get().get();
            return Either.right(result.getId());
        }catch(Exception ex){
            return Either.left(ex.getMessage());
        }
    }
    public Either<String, T> GetById(String id) {
        try{
            DocumentSnapshot entry = collection.document(id).get().get();
        return entry.exists()
                ? Either.right(entry.toObject(classType))
                : Either.left("Data not found for id: "+id);
        }catch(Exception ex){
            return Either.left(ex.getMessage());
        }
    }
}
//RepositoryFactory.java
public class RepositoryFactory{
    private Function<String, CollectionReference> getCollectionReference;
    private final static Map<Class<?>, String> collectionMap = Map.of(
        House.class, "Houses",
        Room.class, "Rooms",
        FurnitureItem.class, "Furniture"
    );   
    public RepositoryFactory(Firestore firestore){
        getCollectionReference = collectionPath -> firestore.collection(collectionPath);
    }
    private RepositoryFactory(DocumentReference document){
        getCollectionReference = collectionPath -> document.collection(collectionPath);
    }
    public  RepositoryFactory FromDocument(Class<?> classType, String documentId){
        String collectionName = collectionMap.get(classType);
        DocumentReference documentReference = getCollectionReference.apply(collectionName).document(documentId);
        return new RepositoryFactory(documentReference);
    }
    public <T> IRepository<T> GetRepository(Class<T> classType){
        String collectionName = collectionMap.get(classType);
        return new FirestoreRepository<T>(getCollectionReference.apply(collectionName), classType);
    }
}
To sum up
This is how I implemented the repository pattern to deal with Firestore subcollections. 
For this example I chose to use Java, but it should be easy to move to other languages.
Open points
In this post I have overlooked some aspects that could be addressed in separate topics:
- How to deal with Google ApiFuture and Java CompletableFuture.
 - How to deal with queries specific for a model.
 
Technologies
- Java
 - Firestore
 - Subcollections
 - Repository Pattern
 
Let me know your way to work with subcollections!
    
Top comments (1)
Well done Giovanni!