DEV Community

Tirumal Rao
Tirumal Rao

Posted on

Implementing the Repository Pattern in Go (Golang)

The Repository Pattern is a commonly used pattern in software architecture that abstracts the data access logic for an application. It promotes a cleaner separation of concerns, making the application more maintainable, scalable, and testable.

When applied to Go (or "Golang"), the Repository Pattern provides a straightforward way to abstract the access to your data source, which could be a database, a web service, or even a file system. Here's a brief overview of how you can implement it:

1. Define your domain model:

// models/user.go
package models

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
}
Enter fullscreen mode Exit fullscreen mode

2. Define the Repository interface:

This interface will outline the methods that your repository must implement.

// repository/user_repository.go
package repository

import "your_package_path/models"

type UserRepository interface {
    GetAll() ([]models.User, error)
    GetByID(id int) (models.User, error)
    Save(user models.User) error
    Delete(id int) error
}
Enter fullscreen mode Exit fullscreen mode

3. Implement the Repository:

For this example, let's assume we're using a simple in-memory store.

// repository/memory/user_repository_impl.go
package memory

import (
    "your_package_path/models"
    "your_package_path/repository"
)

type UserRepositoryMemory struct {
    users []models.User
}

func NewUserRepositoryMemory() repository.UserRepository {
    return &UserRepositoryMemory{
        users: []models.User{},
    }
}

func (r *UserRepositoryMemory) GetAll() ([]models.User, error) {
    return r.users, nil
}

func (r *UserRepositoryMemory) GetByID(id int) (models.User, error) {
    for _, user := range r.users {
        if user.ID == id {
            return user, nil
        }
    }
    return models.User{}, errors.New("User not found")
}

func (r *UserRepositoryMemory) Save(user models.User) error {
    r.users = append(r.users, user)
    return nil
}

func (r *UserRepositoryMemory) Delete(id int) error {
    for index, user := range r.users {
        if user.ID == id {
            r.users = append(r.users[:index], r.users[index+1:]...)
            return nil
        }
    }
    return errors.New("User not found")
}
Enter fullscreen mode Exit fullscreen mode

4. Use the Repository in your services or application logic:

package main

import (
    "your_package_path/models"
    "your_package_path/repository"
    "your_package_path/repository/memory"
)

func main() {
    // Using the in-memory implementation
    repo := memory.NewUserRepositoryMemory()

    user := models.User{
        ID:   1,
        Name: "John Doe",
    }

    repo.Save(user)

    users, _ := repo.GetAll()
    for _, user := range users {
        fmt.Println(user.Name)
    }
}
Enter fullscreen mode Exit fullscreen mode

With this pattern in place, you can easily swap out your in-memory implementation for, say, a PostgreSQL or a MongoDB implementation without changing your main application logic.

Top comments (4)

Collapse
 
gjae profile image
gjae

Could You show us about how to implements this pattern changing between InMemory and InDatabase without instance directly the UserRepositoryMemory? Thanks and great post

Collapse
 
dogers profile image
Dogers

It's not quite a clean implementation as the imports and code in 4 would have to change. You'd need to make it a bit more generic and alias the import so you could do repo := repository.NewRepository(). The idea is you can make lots of 3's which implement different backends.

This smells a lot like MVC without the V :)

Collapse
 
venom90 profile image
Tirumal Rao

You're right; the solution I provided does still tie the main application code to the specific repository implementations, albeit indirectly. Your idea of further abstracting the repository instantiation is a valid one and will provide even greater flexibility.

We can do something like this.

File repository/factory.go

package repository

type RepositoryFactory interface {
    NewUserRepository() UserRepository
}
Enter fullscreen mode Exit fullscreen mode

Then,

// repository/memory/factory.go
package memory

import "your_package_path/repository"

type MemoryFactory struct {}

func (m *MemoryFactory) NewUserRepository() repository.UserRepository {
    return NewUserRepositoryMemory()
}

// repository/database/factory.go
package database

import "your_package_path/repository"

type DatabaseFactory struct {}

func (d *DatabaseFactory) NewUserRepository() repository.UserRepository {
    return NewUserRepositoryDatabase()
}
Enter fullscreen mode Exit fullscreen mode

Usage

package main

import (
    "fmt"
    "your_package_path/models"
    "your_package_path/repository"
    memoryFactory "your_package_path/repository/memory"
    databaseFactory "your_package_path/repository/database"
)

func main() {
    var factory repository.RepositoryFactory

    // You can easily switch between factories here. This decision can be driven by a configuration or environment variable.
    if usingDatabase() { // pseudo-code
        factory = &databaseFactory.DatabaseFactory{}
    } else {
        factory = &memoryFactory.MemoryFactory{}
    }

    repo := factory.NewUserRepository()

    user := models.User{
        ID:   1,
        Name: "John Doe",
    }

    repo.Save(user)

    users, _ := repo.GetAll()
    for _, user := range users {
        fmt.Println(user.Name)
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
venom90 profile image
Tirumal Rao • Edited

One common way to switch between different data store implementations without directly instantiating the specific repositories is to use Dependency Injection (DI) or to use factory functions.

Here's how you can implement this:

1. Implement the Database Repository

Let's first add a mock UserRepositoryDatabase to demonstrate:

Create a file repository/database/user_repository_impl.go

package database

import (
    "errors"
    "your_package_path/models"
    "your_package_path/repository"
)

type UserRepositoryDatabase struct {
    // Let's assume this has a DB connection or some other state
}

func NewUserRepositoryDatabase() repository.UserRepository {
    return &UserRepositoryDatabase{}
}

// Similar functions to UserRepositoryMemory but interacting with a DB
func (r *UserRepositoryDatabase) GetAll() ([]models.User, error) {
    // fetch data from DB (this is a mock)
    return []models.User{{ID: 2, Name: "Database User"}}, nil
}

// ... (other CRUD methods)

Enter fullscreen mode Exit fullscreen mode

2. Create a Factory Function

This factory will decide which repository to return based on some condition (e.g., a configuration setting).
Create a file /repository/factory.go

package repository

import (
    "your_package_path/repository/memory"
    "your_package_path/repository/database"
)

func NewUserRepository(repoType string) UserRepository {
    switch repoType {
    case "memory":
        return memory.NewUserRepositoryMemory()
    case "database":
        return database.NewUserRepositoryDatabase()
    default:
        panic("Unknown repo type")
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Use the Factory in your main application:

This way, you can easily switch between the memory and database implementations just by changing a configuration setting:

package main

import (
    "fmt"
    "your_package_path/models"
    "your_package_path/repository"
)

func main() {
    // Let's assume repoType can be "memory" or "database", and you get it from some config or environment variable
    repoType := "database" // or "memory"

    repo := repository.NewUserRepository(repoType)

    user := models.User{
        ID:   1,
        Name: "John Doe",
    }

    repo.Save(user)

    users, _ := repo.GetAll()
    for _, user := range users {
        fmt.Println(user.Name)
    }
}
Enter fullscreen mode Exit fullscreen mode

By doing this, your main application is abstracted from the actual repository implementation. You can now easily switch between different data storage mechanisms (in-memory, databases, etc.) with minimal changes to the core application logic.