Building web software will always include some kind of database access. For this guide, it does not matter, whether the data access is done via SQL or NoSQL, Google Sheets, whatsoever. Since this post is all about the repository pattern. An often encountered set of classes and interfaces. Let me explain!
What is it used for?
The repository pattern is used to encapsulate data access, so you don't have to spin up a database connection in your controllers or views.
It's often built with the Adapter Pattern in mind. To the extent, that you can swap out the implementation for the database calls. Allowing you to either use a NoSQL, SQL or SQLite database.
How often will I find it?
From my experience, every backend application that talks to some sort of database is using the repository concept in some way. It may not be named that way (which it actually should be), but the concept remains the same.
What are the benefits?
Separation of concerns. Your code will become easier to maintain.
Your code will become cleaner. You don't need to write raw SQL within a controller.
Your code will become more secure. Since you don't write raw SQL at any point, but the repository, you only need to prevent eg. SQL injection at the repository layer
How is it accomplished?
The most basic implementation just consists of one class, with five CRUD operations.
class UserRepository
def create(model_payload); end
def read(id); end
def list(); end
def update(id, model_payload); end
def delete(id); end
end
More generic approaches introduce a superclass, to make building repositories for specific models easier and DRY (Don't repeat yourself).
Please note: We will have a repository for each of our models.
I have built a factory method for a javascript application, that looks like this.
export const StubRepositoryFor = (formDefinition, sampleData = []) => {
const items = sampleData;
return {
create: (data) => {
const item = {id: sampleData.length + 1, ...data};
items.push(item);
console.log("Created ", item);
return item;
},
delete: (id) => {
const index = items.findIndex(c => c.id == id);
items.splice(index, 1);
return true;
},
update: (id, updatePayload) => {
console.log("updating " + id + " with payload", updatePayload);
const itemIndex = items.findIndex(c => c.id == id);
items[itemIndex] = {
...items[itemIndex],
...updatePayload,
};
return {id, ...updatePayload};
},
list: () => {
return items;
},
get: (id) => {
return items.find(c => c.id === id);
},
count: () => {
return items?.length;
},
}
}
With this factory, one can easily create a new repository for a model, like this:
export const UserRepository = StubRepositoryFor([{username: 'IJustDev', first_name: 'Alexander', last_name: 'Panov'}]);
You may have already noticed, that there will be one problem...
Limitations of the Repository factory/superclass
It only works for CRUD resources, which are all managed in the same way.
If you were to introduce this pattern to your front end (which is what I did), you need to make sure, that the API requests are all the same for each model. In other words, there has to be a:
/create
route forstudent
,user
,insert-random-model-name-here
/list
route forstudent
,user
,insert-random-model-name-here
/get/:id
route forstudent
,user
,insert-random-model-name-here
/update/:id
route forstudent
,user
,insert-random-model-name-here
/delete/:id
route forstudent
,user
,insert-random-model-name-here
for every resource (student
,user
, post
, what-ever
).
Using a serverless backend, eg. Firebase, Supabase or AppWrite, will allow you to easily do it.
Having a different backend, which does not conform to the REST standard will make this approach unusable.
Conclusion
A repository is more than just a concept on GitHub. It takes care of keeping your code clean from random database access scattered around hundreds and thousands of files.
Did you learn something new from this post? Never miss valuable knowledge about clean software development by subscribing to my free newsletter.