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

> Learn to create and register new domain models with GORM in Go clean architecture. Define models, implement repository interfaces, and integrate with AutoMigrate for seamless data management.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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.

```go
// 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](https://github.com/manakuro/golang-clean-architecture/blob/main/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.

```go
// 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](https://github.com/manakuro/golang-clean-architecture/blob/main/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.

```go
// 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](https://github.com/manakuro/golang-clean-architecture/blob/main/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.

```go
// 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](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/registry/registry.go)

## Step 5: Configure Auto‑Migration

The infrastructure layer opens the database connection in [`pkg/infrastructure/datastore/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/datastore/db.go). You must explicitly add your new model to the `AutoMigrate` call to create the corresponding table schema.

```go
// 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](https://github.com/manakuro/golang-clean-architecture/blob/main/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.

```go
// 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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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.