How to Create and Register New Domain Models with GORM in Go Clean Architecture

To create and register new domain models with GORM in the golang‑clean‑architecture repository, define a struct with GORM tags in pkg/domain/model, declare the repository interface in pkg/usecase/repository, implement the interface in pkg/adapter/repository using *gorm.DB, and wire it through the pkg/registry while adding the model to AutoMigrate in the infrastructure layer.

The manakuro/golang‑clean‑architecture repository demonstrates a strict Clean Architecture implementation in Go where domain logic remains framework‑agnostic and persistence concerns are isolated through repository abstractions. To persist new entities with GORM, you must bridge the domain layer with the ORM while maintaining the dependency rule that inner layers know nothing of outer layers. This guide walks through the exact steps to create and register new domain models with GORM across each architectural boundary.

Step 1: Define the Domain Model with GORM Tags

Create a new file in pkg/domain/model containing a plain Go struct with GORM struct tags. The domain layer remains pure, containing only data definitions and no business logic, while the tags handle column mapping.

// pkg/domain/model/post.go
package model

import "time"

type Post struct {
    ID        uint       `gorm:"primary_key" json:"id"`
    Title     string     `json:"title"`
    Body      string     `json:"body"`
    AuthorID  uint       `json:"author_id"` // foreign key to User
    CreatedAt *time.Time `json:"created_at"`
    UpdatedAt *time.Time `json:"updated_at"`
    DeletedAt *time.Time `json:"deleted_at"`
}

// TableName overrides the default table name.
func (Post) TableName() string { return "posts" }

Source: pkg/domain/model/user.go

Step 2: Declare the Repository Interface

The use‑case layer declares what the repository must do, not how it does it. Define an interface in pkg/usecase/repository that exposes the required persistence operations.

// pkg/usecase/repository/post.go
package repository

import "golang-clean-architecture/pkg/domain/model"

type PostRepository interface {
    FindAll([]*model.Post) ([]*model.Post, error)
    Create(*model.Post) (*model.Post, error)
    // Extend with Update, Delete, FindByID as needed
}

Source: pkg/usecase/repository/user.go

Step 3: Implement the GORM Adapter

The concrete implementation lives in pkg/adapter/repository. It receives the shared *gorm.DB instance provided by the infrastructure layer and satisfies the interface from step 2.

// pkg/adapter/repository/post.go
package repository

import (
    "golang-clean-architecture/pkg/domain/model"
    "golang-clean-architecture/pkg/usecase/repository"

    "github.com/jinzhu/gorm"
)

type postRepository struct {
    db *gorm.DB
}

// NewPostRepository is invoked from the registry.
func NewPostRepository(db *gorm.DB) repository.PostRepository {
    return &postRepository{db}
}

func (pr *postRepository) FindAll(p []*model.Post) ([]*model.Post, error) {
    if err := pr.db.Find(&p).Error; err != nil {
        return nil, err
    }
    return p, nil
}

func (pr *postRepository) Create(p *model.Post) (*model.Post, error) {
    if err := pr.db.Create(p).Error; err != nil {
        return nil, err
    }
    return p, nil
}

Source: pkg/adapter/repository/user.go

Step 4: Register the Repository in the Registry

The pkg/registry package builds the object graph. Extend the registry struct to expose a constructor for the new repository so the DB connection is injected automatically.

// pkg/registry/registry.go (excerpt)
type registry struct {
    db *gorm.DB
}

func NewRegistry(db *gorm.DB) Registry {
    return &registry{db}
}

// NewPostRepository wires the GORM DB instance into the adapter.
func (r *registry) NewPostRepository() repository.PostRepository {
    return repository.NewPostRepository(r.db)
}

Source: pkg/registry/registry.go

Step 5: Configure Auto‑Migration

The infrastructure layer opens the database connection in pkg/infrastructure/datastore/db.go. You must explicitly add your new model to the AutoMigrate call to create the corresponding table schema.

// pkg/infrastructure/datastore/db.go (excerpt)
func NewDB() *gorm.DB {
    // ... existing connection logic ...
    db, err := gorm.Open(DBMS, mySqlConfig.FormatDSN())
    if err != nil {
        log.Fatalln(err)
    }

    // Add every domain struct you want persisted:
    db.AutoMigrate(&model.User{}, &model.Post{})
    return db
}

Source: pkg/infrastructure/datastore/db.go

Step 6: Consume the Repository in Use Cases

Create a use‑case service that receives the PostRepository via constructor injection, keeping the dependency pointing inward toward the domain.

// pkg/usecase/usecase/post.go
package usecase

import (
    "golang-clean-architecture/pkg/domain/model"
    "golang-clean-architecture/pkg/usecase/repository"
)

type Post struct {
    repo repository.PostRepository
}

func NewPost(repo repository.PostRepository) Post {
    return Post{repo}
}

func (p Post) GetAll() ([]*model.Post, error) {
    return p.repo.FindAll([]*model.Post{})
}

func (p Post) Create(post *model.Post) (*model.Post, error) {
    return p.repo.Create(post)
}

Summary

  • Domain models are defined in pkg/domain/model with GORM struct tags and optional TableName methods.
  • Repository contracts are declared in pkg/usecase/repository as Go interfaces, ensuring the use‑case layer depends on abstractions.
  • Concrete implementations live in pkg/adapter/repository and accept *gorm.DB from the infrastructure layer.
  • Dependency injection is handled in pkg/registry, which constructs repository instances with the active DB connection.
  • Schema management requires adding the new model to db.AutoMigrate() inside pkg/infrastructure/datastore/db.go.
  • Use‑case services consume repository interfaces via constructor injection, maintaining Clean Architecture’s dependency rule.

Frequently Asked Questions

Where should GORM struct tags be placed in Clean Architecture?

GORM struct tags belong directly on the domain model fields in pkg/domain/model. While the domain layer should remain agnostic to persistence details, GORM tags are interpreted metadata rather than import dependencies, allowing the struct to live in the domain layer without violating the dependency rule. The infrastructure layer imports the domain model to perform the actual database operations.

How do I handle database migrations for new domain models?

Add your new model struct to the AutoMigrate call in pkg/infrastructure/datastore/db.go. The repository uses github.com/jinzhu/gorm (GORM v1), so db.AutoMigrate(&model.Post{}) automatically creates or updates the corresponding table schema when the application starts. For production environments, consider implementing separate migration files rather than relying solely on AutoMigrate.

Why does the FindAll repository method accept a slice parameter?

The FindAll([]*model.Post) signature in the userRepository implementation follows the pattern shown in the source code, where the slice acts as both input and output destination for GORM’s Find method. This allows GORM to populate the provided slice in‑place while still returning it for functional chaining. You may alternatively use a pointer to an empty slice if you prefer a zero‑value input pattern.

Can I use GORM v2 with this clean architecture pattern?

Yes. While the repository currently imports github.com/jinzhu/gorm (v1), you can substitute gorm.io/gorm (v2) by updating the import paths in pkg/adapter/repository and pkg/infrastructure/datastore. The repository interface in the use‑case layer remains unchanged because it depends only on domain models, not the specific GORM version, preserving the architecture’s flexibility.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →