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
}
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:
- Test…
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
}
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
}
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
}
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
}
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
}
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)
}
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))
}
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)
}
}
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);
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
Key Advantages
- Easy Testing: Swap PostgreSQL for in-memory implementation
- Database Independence: Change databases without touching business logic
- Clean Code: Each layer has one responsibility
- 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
- GitHub: https://github.com/mayrafc/golang-repository-pattern.git
- Martin Fowler’s Book: “Patterns of Enterprise Application Architecture”