DEV Community

Mayra
Mayra

Posted on

Repository Pattern in Golang: A Practical Guide

The Repository Pattern, documented in Martin Fowler's "Patterns of Enterprise Application Architecture," separates data access logic from business logic. This article demonstrates its implementation in Golang through a real-world e-commerce example.

The Problem
Without proper separation, your code mixes business logic with database operations:

func ProcessOrder(orderID string) error {
    db.Query("SELECT * FROM orders WHERE id = ?", orderID)
    // Business rules buried in database code
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • Tight coupling to database
  • Hard to test
  • Code duplication
  • Can't switch databases easily

The Solution: Repository Pattern

The pattern provides an abstraction layer between business logic and data access, treating data like an in-memory collection.

Benefits:

  • Testability (easy to mock)
  • Flexibility (switch databases)
  • Clean separation of concerns
  • Centralized data access

Implementation

Domain Model

// domain/product.go
package domain

import "time"

type Product struct {
    ID          string
    Name        string
    Price       float64
    Stock       int
    Category    string
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

func (p *Product) Validate() error {
    if p.Name == "" {
        return errors.New("name required")
    }
    if p.Price < 0 {
        return errors.New("invalid price")
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Repository Interface

// repository/interface.go
package repository

import (
    "context"
    "github.com/yourusername/ecommerce-repository/domain"
)

type ProductRepository interface {
    Create(ctx context.Context, product *domain.Product) error
    FindByID(ctx context.Context, id string) (*domain.Product, error)
    FindAll(ctx context.Context) ([]*domain.Product, error)
    FindByCategory(ctx context.Context, category string) ([]*domain.Product, error)
    Update(ctx context.Context, product *domain.Product) error
    Delete(ctx context.Context, id string) error
}
Enter fullscreen mode Exit fullscreen mode

PostgreSQL Implementation

// repository/postgres.go
package repository

import (
    "context"
    "database/sql"
    "github.com/yourusername/ecommerce-repository/domain"
)

type PostgresProductRepository struct {
    db *sql.DB
}

func NewPostgresProductRepository(db *sql.DB) *PostgresProductRepository {
    return &PostgresProductRepository{db: db}
}

func (r *PostgresProductRepository) Create(ctx context.Context, product *domain.Product) error {
    query := `
        INSERT INTO products (id, name, price, stock, category, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7)
    `
    _, err := r.db.ExecContext(ctx, query,
        product.ID, product.Name, product.Price, 
        product.Stock, product.Category, time.Now(), time.Now())
    return err
}

func (r *PostgresProductRepository) FindByID(ctx context.Context, id string) (*domain.Product, error) {
    query := `SELECT id, name, price, stock, category, created_at, updated_at 
              FROM products WHERE id = $1`

    product := &domain.Product{}
    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &product.ID, &product.Name, &product.Price,
        &product.Stock, &product.Category, &product.CreatedAt, &product.UpdatedAt)

    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("product not found")
    }
    return product, err
}

func (r *PostgresProductRepository) FindByCategory(ctx context.Context, category string) ([]*domain.Product, error) {
    query := `SELECT id, name, price, stock, category, created_at, updated_at 
              FROM products WHERE category = $1`

    rows, err := r.db.QueryContext(ctx, query, category)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*domain.Product
    for rows.Next() {
        p := &domain.Product{}
        err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock, 
                        &p.Category, &p.CreatedAt, &p.UpdatedAt)
        if err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, nil
}

func (r *PostgresProductRepository) Update(ctx context.Context, product *domain.Product) error {
    query := `UPDATE products SET name=$1, price=$2, stock=$3, 
              category=$4, updated_at=$5 WHERE id=$6`
    _, err := r.db.ExecContext(ctx, query, product.Name, product.Price,
        product.Stock, product.Category, time.Now(), product.ID)
    return err
}

func (r *PostgresProductRepository) Delete(ctx context.Context, id string) error {
    query := `DELETE FROM products WHERE id = $1`
    _, err := r.db.ExecContext(ctx, query, id)
    return err
}
Enter fullscreen mode Exit fullscreen mode

In-Memory Implementation (Testing)

// repository/memory.go
package repository

import (
    "context"
    "sync"
    "github.com/yourusername/ecommerce-repository/domain"
)

type InMemoryProductRepository struct {
    mu       sync.RWMutex
    products map[string]*domain.Product
}

func NewInMemoryProductRepository() *InMemoryProductRepository {
    return &InMemoryProductRepository{
        products: make(map[string]*domain.Product),
    }
}

func (r *InMemoryProductRepository) Create(ctx context.Context, product *domain.Product) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.products[product.ID] = product
    return nil
}

func (r *InMemoryProductRepository) FindByID(ctx context.Context, id string) (*domain.Product, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    product, exists := r.products[id]
    if !exists {
        return nil, fmt.Errorf("not found")
    }
    return product, nil
}

func (r *InMemoryProductRepository) FindByCategory(ctx context.Context, category string) ([]*domain.Product, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    var products []*domain.Product
    for _, p := range r.products {
        if p.Category == category {
            products = append(products, p)
        }
    }
    return products, nil
}
Enter fullscreen mode Exit fullscreen mode

Business Service

// service/product_service.go
package service

import (
    "context"
    "github.com/yourusername/ecommerce-repository/domain"
    "github.com/yourusername/ecommerce-repository/repository"
)

type ProductService struct {
    repo repository.ProductRepository
}

func NewProductService(repo repository.ProductRepository) *ProductService {
    return &ProductService{repo: repo}
}

func (s *ProductService) CreateProduct(ctx context.Context, product *domain.Product) error {
    if err := product.Validate(); err != nil {
        return err
    }
    return s.repo.Create(ctx, product)
}

func (s *ProductService) GetProduct(ctx context.Context, id string) (*domain.Product, error) {
    return s.repo.FindByID(ctx, id)
}

func (s *ProductService) GetProductsByCategory(ctx context.Context, category string) ([]*domain.Product, error) {
    return s.repo.FindByCategory(ctx, category)
}
Enter fullscreen mode Exit fullscreen mode

Main Application

// main.go
package main

import (
    "database/sql"
    "log"
    "net/http"
    "github.com/gorilla/mux"
    _ "github.com/lib/pq"
)

func main() {
    db, _ := sql.Open("postgres", "postgres://user:pass@localhost/ecommerce?sslmode=disable")
    defer db.Close()

    // Switch implementations easily
    repo := repository.NewPostgresProductRepository(db)
    // Or use: repo := repository.NewInMemoryProductRepository()

    service := service.NewProductService(repo)

    r := mux.NewRouter()
    r.HandleFunc("/products", createProduct(service)).Methods("POST")
    r.HandleFunc("/products/{id}", getProduct(service)).Methods("GET")
    r.HandleFunc("/products", listProducts(service)).Methods("GET")

    log.Fatal(http.ListenAndServe(":8080", r))
}
Enter fullscreen mode Exit fullscreen mode

Testing Example

func TestProductService(t *testing.T) {
    repo := repository.NewInMemoryProductRepository()
    service := service.NewProductService(repo)

    product := &domain.Product{
        ID: "1", Name: "Mouse", Price: 29.99, Stock: 50, Category: "electronics"}

    err := service.CreateProduct(context.Background(), product)
    if err != nil {
        t.Fatal(err)
    }

    found, _ := service.GetProduct(context.Background(), "1")
    if found.Name != "Mouse" {
        t.Errorf("expected Mouse, got %s", found.Name)
    }
}
Enter fullscreen mode Exit fullscreen mode

Database Schema

CREATE TABLE products (
    id VARCHAR(255) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INTEGER NOT NULL,
    category VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_category ON products(category);
Enter fullscreen mode Exit fullscreen mode

Running the Project

# Setup
go mod init github.com/yourusername/ecommerce-repository
go get github.com/gorilla/mux github.com/lib/pq

# Run with PostgreSQL
DATABASE_URL="postgres://user:pass@localhost/ecommerce" go run main.go

# Run with in-memory (testing)
USE_MEMORY=true go run main.go

# Test API
curl -X POST http://localhost:8080/products \
  -d '{"id":"1","name":"Mouse","price":29.99,"stock":50,"category":"electronics"}'

curl http://localhost:8080/products/1
curl http://localhost:8080/products?category=electronics
Enter fullscreen mode Exit fullscreen mode

Key Advantages

  1. Easy Testing: Swap PostgreSQL for in-memory implementation
  2. Database Independence: Change databases without touching business logic
  3. Clean Code: Each layer has one responsibility
  4. Maintainability: All queries in one place

When to Use
Use Repository Pattern when:

  • Complex business logic
  • Need to support multiple databases
  • High test coverage required
  • Large team with separate concerns

Avoid when:

  • Simple CRUD apps
  • Prototypes
  • Very small projects

Conclusion
The Repository Pattern provides a clean separation between business logic and data access. This example shows how to implement it in Golang with real code that you can use in production.

References

Top comments (0)