Why SHOULD you use the Repository Pattern? And how to do it, the easy way…

Mihai Blebea
9 min readJun 2, 2019

--

The last thing you need is another pattern to worry about…

Why can’t we just write code in a plain simple way?

Well… if you ask me, patterns have a very important role in software development.

Design Patterns VS Simple code

There is no such thing as plain simple code.

Even if you don’t know any patterns, you still use one every time you write code. It’s called “spaghetti code pattern” 😊

Yes, it might sound delicious after some long coding hours in the middle of the night, but believe me, it's a developer’s worst nightmare.

Patterns are your best friend because they help you organize your code in a way that is clear to read, flexible to extends and easy to reason with.

Patterns can make your life a lot easier and can improve the speed at which you can add more features to your project in the future.

  1. So what is the Base Repository pattern supposed to do?
  2. How does it look like?
  3. And what are his main benefits?

Base Repository pattern brings in a layer of abstraction implemented between your models (domain logic) and persistence layer (database).

It helps you decouple the models from the persistence layer, so in the future, you can easily change the database tables without affecting the logic of your app.

You can even change your whole database implementation, your models should still not care about that.

Talking about some ungrateful domain models, right? 😊

Let’s look at some code, so you can grasp the concept better

I will use Typescript for this, mostly because it brings strong types and interfaces to javascript. And also because I use it every day in my job. 😊

If you don’t know much about Typescript, I suggest you read this first: Typescript is Javascript with superpowers

Now, let’s take a look back, down the memory lane…

This is how I used to persist models into the database:

import { User } from './../models'let user = new User('Bob', 'Smith', 29, 'front end developer')user.persiste()

And inside the User model:

import { myslqConnection } from './mysql-connection'export default class User
{
private _firstName : string
private _lastName : string private _age : number private _job : string constructor(
firstName : string,
lastName : string,
age : number,
job : string
){
this._firstName = firstName
this._lastName = lastName
this._age = age
this._job = job
}
persist()
{
// Somehow you need to pass in the configs for connecting to the database
return myslqConnection.query(`
INSERT INTO users
(first_name, last_name, age, job)
VALUES (?)`, [
this.firstName,
this.lastName,
this.age,
this.job ])
}
}

This doesn’t look right.

Here are some reasons why this is an absolute disaster:

  1. I was mixing the model as in business logic with the persistence layer. Obs: The User model should not know how it is persisted in the database because it doesn’t care about that. This ungrateful User models, they don’t care about anything… 😊
  2. I was implementing the connection to the database in the actual model, which is a bad thing if you ever want to change the credentials.

There are a lot of other reasons why this implementation is bad, but I won’t bore you with the details…

One last thing that I want to point out

Imagine that on Monday morning, the Project Manager comes to your desk and says:

“We decided to change our current Mysql database to Mongo DB. We think that it will be much faster. Your job is to migrate our current stack to make it work with the new DB… “

  1. That means that you will have to update all of your models
  2. Make sure your main logic is not broken
  3. Redo every automatic test that you have on those models

Imagine the HORROR…

Don’t throw your laptop through the window yet, because there is a solution.

Repositories to the rescue

Now let’s see how we can implement this layer in between the models and database.

  1. We can put the persistence logic into a BaseModel class and then have all our models extend that. Hmm that won’t solve anything, in fact, it will complicate things a little bit more
  2. We can use an ORM package that will abstract all the bits and bobs. Yeah, that can help a bit, but then we will still couple the anemic models with the ORM logic. In case you need to change the ORM, you will have the same problem as before.
  3. We can use a different class that can handle the persistence logic and then pass in the models to be saved to the database. Hmm, that sounds like a much better idea…

Let’s try it.

But before starting to write this “special” class to handle the saving and retrieving from the database, let’s think outside the box and start with how our implementation should look like from the outside.

  1. We want a clean way to handle the models
  2. We don’t want to pass the database credentials in the constructor of this class every time we need to use it
  3. We want to use the same interface in case we ever need to change the database type

We want something like this…

import { connexion } from './connexion'let repo = new UserRepo(connexion) // this is the connexion to the database// Create the user model
let user = new User('Bob', 'Smith', 29, 'front end developer')
// Save the user model to the database
repo.create(user)
// Get a user from the database
let otherUser = repo.findOne({ id: 1 })
// DONE, simple as that

You see…

We abstracted all the logic that handles the inserting and selecting, inside this UserRepo class.

If we ever need to change the database, we can do it without affecting the business logic of our app.

I will show you how to implement this pattern from scratch and then, so let’s start with creating the BaseRepository class.

This will be an abstract class that can hold the methods that we are going to share for all our Repositories.

How to build a BaseRepository abstract class in Typescript

import Context from './Context'export default abstract class BaseRepository<T>
{
protected _context : Context constructor(context : Context) {
this._context = context
}
create(model : T) : Promise<null> {
throw new Error('Method not implemented.')
}
update(id : number, model : T) : Promise<null> {
throw new Error('Method not implemented.')
}
delete(id : number) : Promise<null> {
throw new Error('Method not implemented.')
}
all() : Promise<T[]> {
throw new Error('Method not implemented.')
}
findOne(id : number) : Promise<T> {
throw new Error('Method not implemented.')
}
}

As you can see, we have an abstract class that holds methods for creating, updating, deleting entities and also for getting all and finding one by id.

These are all the methods that we will share between the concrete implementations.

If you are wondering what is the Context class that gets passed in the constructor, that is a way to abstract the connection to the database.

This is just a nice way to keep everything that needs to change for the same reason in a single place.

Inside, the Context class looks like this:

import { Connection, createConnection } from 'mysql' // npm install mysqlexport default class Context {    private _conn? : Connection    private _host : string    private _database : string    private _username : string    private _password : string    private _port : number    constructor(
host : string,
database : string,
username : string,
password : string,
port? : number)
{
this._host = host
this._database = database
this._username = username
this._password = password
this._port = port || 3306 //optional and default 3306
}
connect() {
this._conn = createConnection({
host: this._host,
user: this._username,
database: this._database,
password: this._password,
port: this._port
})
}
private _assertConnectionInitialized() {
if(!this._conn) {
throw Error('Connection is not initialized')
}
}

// Wrapping the callback in a Promise
async query(query : string, params? : any[]) : Promise<any> {
// Check if the connection is initialized
this._assertConnectionInitialized()
// Return a Promise and handle the callback inside it
return new Promise((resolve, reject)=> {
this._conn!.query(query, params, (err, result)=> {
if(err) { return reject(error) }
return resolve(result)
})
})
}
// You shouldn't need this, but it's always good to have
get connection() {
return this._connection
}
// This is going to be useful for transactions
rollback() {
this._assertConnectionInitialized()
this._conn!.rollback()
this._conn!.end()
}
// Use this to close the connection or commit the transaction
complete() {
this._assertConnectionInitialized()
this._conn!.commit()
this._conn!.end()
}
}

You can choose to skip the Context class and just initialize the connection to the database inside the UserRepository or BaseRepository, but I think this is a cleaner way to do it.

Just imagine passing the sensitive credentials every time you call a Repository class. It will make it much harder to change the connection configs when you need to.

Now let’s build an implementation of this class in the form of a UserRepository.

Our UserRepository will extend the BaseRepository and will use the Context object to get a connection to the database and do all sorts of queries.

import Context from './Context'
import BaseRepository from './BaseRepository'
import { User, IUserRepository } from './../../Domain'
import UserTransformer from './UserTransformer' // will explain later
export default class UserRepository extends BaseRepository<User> implements IUserRepository { constructor(context : Context) {
super(context)
}
async findOne(id : number) {
this._context.connect()
let rows = await this._context.query(
`SELECT * FROM clients WHERE id = ?`, [ id ]
this._context.complete()
return ClientTransformer.toModel(rows)[0]
}
async all() {
this._context.connect()
let rows = await this._context.query(
`SELECT * FROM clients`)
this._context.complete()
return ClientTransformer.toModel(rows)
}
async create(model : Client) {
let raw = ClientTransformer.toRaw(model)
let values = Object.values(raw)
let columns = Object.keys(raw).join(',')
this._context.connect()
await this._context.query(
`INSERT INTO clients
(${ columns }) VALUES (?)`, [ values ])
this._context.complete()
return null
}
async update(id : number, model : Client) {
let raw = ClientTransformer.toRaw(model)
let pairs = Object.keys(raw).map((key)=> {
return `${ key } = ?`
}).join(',')
let values = Object.values(raw)
this._context.connect()
await this._context.query(
`UPDATE clients
SET ${ pairs } WHERE id = ?`, [ ...values, id ])
this._context.complete()
return null
}
async delete(id : number) {
this._context.connect()
await this._context.query(
`DELETE FROM clients WHERE id = ?`, [ id ])
this._context.complete()
return null
}
}

You may be wondering why I choose to return null from the create, delete and update methods.

This is because of the principle that the creator of CQS (Command Query Separation), Bertrand Meyer, formulated:

Asking a question should not change the answer

You can read more about the Command Query Separation here.

But in short terms, we want to separate the INSERT, DELETE, UPDATE as Commands that return nothing and the SELECT as Query which returns the entity or entities that you are expecting.

Let’s take a closer look at the UserTransformer.

No, it’s not part of the Decepticons 😊.

It is just a way to map the domain model properties to table columns and from database table columns to a domain model.

Remember that we don’t want to dirty our hands with persistence logic, we want to receive back from our Repos the already built models.

So how does the UserTransformer looks like?

import { User } from './../../Domain'export default abstract class UserTransformer {    static toModel(rows : { [key : string] : any }[]) {
return rows.map((row)=> {
return new User(
row.first_name,
row.last_name,
row.age,
row.job)
})
}
static toRaw(model : User) {
return {
first_name: model.firstName,
last_name: model.lastName,
age: model.age,
}
}
}

All that it does is transform the model to row data, and transform the raw data into a User model.

I hope this helps you get a better understanding of how the Base Repository pattern works and why it’s a good idea to implement some sort of layer in between your domain logic and persistence layer.

But there are a couple of things that I would still improve on this…

How can you improve your Repositories beyond this article implementation?

  1. You can choose to not hold a reference to the database connection in the Context class.
  2. You can use a library that already wraps the mysql queries in a Promise. Check this out: https://www.npmjs.com/package/promise-mysql
  3. You can split your UserRepository into a WriteUserRepository and ReadUserRepository. This way you have a better separation of concerns.
  4. This is an open list so feel free to add more improvements in the comment section below. I would love to see what you come up with.

One major problem with using a Base Repository pattern is that you end up with one million dependencies that you need to take care of.

Don’t worry about that, because you can always use a Dependency Container to handle all your… well… dependencies.😊

If you already use one, that’s perfect.

But if you don’t, then take a look over this one, which I built and use in all of my projects.

Use Typescript Dependency Injection: https://www.npmjs.com/package/typescript-dependency-injection

It’s an open source project, so feel free to contribute.

It will help NOT write this every time you need to select a User model from the database…

import { Context, UserRepository } from './somewhere-far-far-away'let context = new Context('host', 'databse_name', 'username', 'password')let repository = new UserRepository(context)let user = repository.findOne({ id: 23 })

Instead, you can do this just once:

import { Container } from 'typescript-dependency-injection'
import { Context, UserRepository } from './somewhere-far-far-away'
let container = new Container()container.register(Context)
.dependsOnString('host')
.dependsOnString('databse_name')
.dependsOnString('username')
.dependsOnString('password')
container.register(UserRepository)
.dependsOnClass('Context')
export default container

And then, every time you need the UserRepository, you can do this:

import container from './where-ever-the-container-is'let repo = container.get('UserRepository')// DONE

Don’t forget to give me a clap if you liked the article or write me a comment in the section below.

--

--

Mihai Blebea

London based Head of FullStack Development for leading dating app. Love coding, software architecture and WW2 history youtube channels.