Adding New Repository Implementations for Database Operations in Golang Clean Architecture
You add new repository implementations by creating an adapter package that fulfills the use-case layer's interface contract, then wiring it into the registry without modifying business logic.
This guide demonstrates adding new repository implementations for database operations in the manakuro/golang-clean-architecture project. The codebase follows Clean Architecture principles where domain logic remains pure while data access details live in replaceable adapter layers. By adhering to the contract-first pattern, you can introduce MongoDB, PostgreSQL, or in-memory stores without altering use-case code.
How the Repository Pattern Is Structured
The architecture enforces strict separation through four distinct layers. Each layer owns specific responsibilities and communicates through defined interfaces rather than concrete types.
Layer Responsibilities
- Domain Layer (
pkg/domain/model/user.go): Contains pure entities like theUserstruct with only JSON tags and zero external dependencies. - Use-Case Layer (
pkg/usecase/repository/user.go): Defines the repository contract via theUserRepositoryinterface, declaring methods likeFindAllandCreatewithout implementation details. - Adapter Layer (
pkg/adapter/repository/user.go): Provides concrete data-access logic, such as GORM-based SQL operations, by implementing the use-case interfaces. - Registry (
pkg/registry/registry.go): Handles dependency injection, mapping concrete adapter implementations to the abstract interfaces required by use-case services.
The Contract-First Design
The UserRepository interface in pkg/usecase/repository/user.go establishes the boundary:
type UserRepository interface {
FindAll(u []*model.User) ([]*model.User, error)
Create(u *model.User) (*model.User, error)
}
For transaction support, the DBRepository interface in pkg/usecase/repository/db.go provides a generic wrapper:
type DBRepository interface {
Transaction(func(interface{}) (interface{}, error)) (interface{}, error)
}
Business logic in the use-case layer depends only on these interfaces, remaining ignorant of whether data persists in MySQL, MongoDB, or memory.
Step-by-Step Guide to Adding a New Repository
To add a MongoDB implementation alongside the existing GORM adapter, follow this contract-based workflow.
1. Create the Adapter Package
Create a new directory pkg/adapter/repository/mongo to house the MongoDB-specific implementation. This keeps the adapter layer modular and allows multiple storage engines to coexist.
2. Implement the UserRepository Interface
Define a struct that holds the MongoDB collection reference and implement all methods from the UserRepository interface:
// pkg/adapter/repository/mongo/user.go
type mongoUserRepo struct {
coll *mongo.Collection
}
func (r *mongoUserRepo) FindAll(_ []*model.User) ([]*model.User, error) {
cursor, err := r.coll.Find(context.Background(), bson.D{})
if err != nil {
return nil, err
}
var users []*model.User
if err = cursor.All(context.Background(), &users); err != nil {
return nil, err
}
return users, nil
}
func (r *mongoUserRepo) Create(u *model.User) (*model.User, error) {
_, err := r.coll.InsertOne(context.Background(), u)
if err != nil {
return nil, err
}
return u, nil
}
3. Expose a Factory Function
Provide a constructor that matches the signature expected by the registry, accepting infrastructure dependencies and returning the interface type:
func NewMongoUserRepository(client *mongo.Client) repository.UserRepository {
coll := client.Database("myapp").Collection("users")
return &mongoUserRepo{coll: coll}
}
4. Wire Into the Registry
Modify pkg/registry/registry.go to instantiate the new repository. Import the mongo adapter and update the RegisterRepositories function to accept the MongoDB client:
import (
mongoRepo "github.com/manakuro/golang-clean-architecture/pkg/adapter/repository/mongo"
)
func RegisterRepositories(db *gorm.DB, mongoClient *mongo.Client) *Registry {
r := &Registry{}
r.UserRepo = mongoRepo.NewMongoUserRepository(mongoClient)
return r
}
5. Update the Entry Point
Adjust cmd/app/main.go to initialize the MongoDB connection and pass it to the registry:
mongoClient, err := mongo.Connect(context.Background(), options.Client().ApplyURI(cfg.MongoURI))
if err != nil {
log.Fatal(err)
}
reg := registry.RegisterRepositories(gormDB, mongoClient)
The use-case services remain unchanged because they depend only on the UserRepository interface, not the concrete MongoDB type.
Complete Working Example
After wiring the new repository, the application entry point constructs services that automatically use MongoDB without code changes in the business layer:
package main
import (
"context"
"log"
"github.com/manakuro/golang-clean-architecture/pkg/registry"
"github.com/manakuro/golang-clean-architecture/pkg/usecase"
"github.com/manakuro/golang-clean-architecture/pkg/domain/model"
)
func main() {
// Registry initialized with MongoDB repository
reg := registry.Init()
// Service uses UserRepository interface; implementation detail is invisible
userSvc := usecase.NewUserService(reg.UserRepo)
newUser := &model.User{Name: "Alice", Email: "alice@example.com"}
created, err := userSvc.Create(context.Background(), newUser)
if err != nil {
log.Fatalf("create error: %v", err)
}
users, err := userSvc.ListAll(context.Background())
if err != nil {
log.Fatalf("list error: %v", err)
}
log.Printf("found %d users", len(users))
}
Summary
- Define contracts in the use-case layer: Interfaces like
UserRepositoryinpkg/usecase/repository/user.goabstract storage details from business logic. - Implement in the adapter layer: Create new packages under
pkg/adapter/repository/that fulfill these interfaces with specific technology code (GORM, MongoDB driver, etc.). - Wire through the registry: Update
pkg/registry/registry.goto map concrete implementations to interfaces, keeping dependency injection centralized. - Preserve domain purity: The
pkg/domain/model/structs carry no import dependencies, ensuring business rules remain storage-agnostic.
Frequently Asked Questions
Can I maintain multiple repository implementations simultaneously?
Yes. The registry pattern allows you to instantiate both SQL and NoSQL repositories in pkg/registry/registry.go and inject them into different use-case services. Simply create separate factory functions like NewUserRepository for GORM and NewMongoUserRepository for MongoDB, then assign each to the appropriate service constructor.
How do I handle transactions when adding a new database type?
Implement the DBRepository interface from pkg/usecase/repository/db.go in your new adapter package. This interface requires a Transaction method that accepts a function wrapper. Your implementation must handle connection lifecycle management—whether through GORM's Transaction method or MongoDB's multi-document ACID sessions—while exposing the same atomicity contract to the use-case layer.
Do I need to modify the domain models when changing storage engines?
No. Domain models in pkg/domain/model/user.go are pure structs with only JSON tags and validation logic. They contain no ORM-specific tags or database dependencies, allowing them to serialize to MongoDB BSON, SQL relational tables, or API JSON payloads without modification.
How do I test a new repository implementation without running the full application?
Create an in-memory implementation of UserRepository in a test package. Implement FindAll and Create using a map[string]*model.User or slice backing store. Pass this stub to your use-case service in unit tests to verify business logic without database dependencies, then integration-test the concrete MongoDB or SQL adapter separately.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →