# Running Database Migrations and Schema Management with GORM in Golang Clean Architecture

> Master GORM database migrations and schema management in Golang Clean Architecture. Learn how to ensure explicit schema evolution and production safety with clear version control.

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

---

**The manakuro/golang-clean-architecture repository uses Goose for version-controlled SQL migrations while leveraging GORM strictly for runtime database operations, ensuring explicit schema evolution and production safety.**

This guide examines how the [golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture) project handles database migrations and schema management with GORM. Rather than relying on GORM's automatic migration features, the codebase implements a deliberate separation between schema evolution (managed by the Goose CLI) and data access (handled by GORM repositories).

## Understanding the Hybrid Migration Architecture

The repository follows a **dual-tool strategy** that distinguishes between schema definition and runtime ORM operations. This approach prevents accidental schema changes in production while maintaining type-safe database interactions.

**Goose** handles all structural changes through versioned SQL files stored in `db/migrations/`. **GORM** manages connection pooling, query building, and struct-to-table mapping at runtime. This separation ensures that database schema changes are explicit, code-reviewed, and reversible.

## Setting Up the GORM Database Connection

The database connection is established in [`pkg/infrastructure/datastore/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/datastore/db.go), where the `NewDB` function constructs a MySQL DSN from environment configuration and returns a `*gorm.DB` instance.

```go
package main

import (
    "golang-clean-architecture/pkg/infrastructure/datastore"
    "golang-clean-architecture/pkg/adapter/repository"
)

func main() {
    db := datastore.NewDB() // Returns *gorm.DB configured for MySQL
    
    // Inject into repository layer
    userRepo := repository.NewUserRepository(db)
    // Repository is now ready for CRUD operations
}

```

The connection function handles DSN formatting, connection pooling, and GORM configuration without executing any schema modifications.

## Managing Schema Changes with Goose Migrations

Schema evolution is managed entirely through Goose migration files located in `db/migrations/`. These SQL files are timestamped and version-controlled, providing an immutable history of database changes.

### Creating Migration Files

Migration files follow the Goose annotation format with explicit `Up` and `Down` sections. The following example from [`db/migrations/20190802195721_createUsers.sql`](https://github.com/manakuro/golang-clean-architecture/blob/main/db/migrations/20190802195721_createUsers.sql) creates the `users` table that maps to the GORM model:

```sql
-- +goose Up
-- +goose StatementBegin
CREATE TABLE users (
   id INT NOT NULL AUTO_INCREMENT,
   name varchar(255) DEFAULT NULL COMMENT 'user name',
   age varchar(255) DEFAULT NULL COMMENT 'age',
   created_at datetime DEFAULT NULL COMMENT 'created at',
   updated_at datetime DEFAULT NULL COMMENT 'updated at',
   deleted_at timestamp NULL DEFAULT NULL COMMENT 'deleted at',
   INDEX user_id (id),
   PRIMARY KEY(id)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user';
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE users;
-- +goose StatementEnd

```

### Running Migrations via Make

The `Makefile` provides convenience targets for executing Goose commands without memorizing CLI flags:

```bash

# Create the MySQL database (runs ./bin/init_db.sh)

make setup_db

# Apply all pending up-migrations

make migrate_schema_up

# Roll back the last migration

make migrate_schema_down

# Reset the schema (down then up)

make migrate_schema_reset

```

These commands ensure that schema changes happen explicitly during deployment rather than automatically at application startup.

## Mapping GORM Models to Database Tables

Domain models in [`pkg/domain/model/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/domain/model/user.go) define GORM structs that map to tables created by Goose migrations. The `User` struct includes GORM tags for column mapping and soft-delete support:

```go
package model

import "time"

type User struct {
    ID        uint       `gorm:"primary_key" json:"id"`
    Name      string     `gorm:"column:name" json:"name"`
    Age       string     `gorm:"column:age" json:"age"`
    CreatedAt time.Time  `gorm:"column:created_at" json:"created_at"`
    UpdatedAt time.Time  `gorm:"column:updated_at" json:"updated_at"`
    DeletedAt *time.Time `gorm:"column:deleted_at" sql:"index" json:"deleted_at"`
}

```

The struct fields align exactly with the SQL schema defined in the Goose migration, ensuring type safety without runtime schema modification.

## Implementing the Repository Layer

The repository implementation in [`pkg/adapter/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user.go) receives the GORM instance via `NewUserRepository` and provides CRUD operations. **No automatic migration occurs at runtime**—the repository assumes the schema already exists:

```go
package repository

import (
    "golang-clean-architecture/pkg/domain/model"
    "gorm.io/gorm"
)

type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) FindAll() ([]*model.User, error) {
    var users []*model.User
    result := r.db.Find(&users)
    return users, result.Error
}

func (r *UserRepository) Create(user *model.User) (*model.User, error) {
    result := r.db.Create(user)
    return user, result.Error
}

```

This design enforces the separation between schema management (Goose) and data access (GORM).

## Development Workflow for Database Changes

When modifying the database schema in this codebase, follow this explicit workflow:

1. **Create a new Goose migration file** in `db/migrations/` with a timestamp prefix (e.g., [`20240115120000_addEmailColumn.sql`](https://github.com/manakuro/golang-clean-architecture/blob/main/20240115120000_addEmailColumn.sql))
2. **Write the SQL** for both `Up` and `Down` operations to ensure reversibility
3. **Run `make migrate_schema_up`** to apply the change to your local database
4. **Update the GORM model** in `pkg/domain/model/` to reflect the new schema (e.g., add an `Email` field)
5. **Update repository methods** if needed to handle the new columns
6. **Commit both the SQL migration and the code changes** to maintain synchronization between schema and models

## Why Avoid GORM AutoMigrate in Production

The repository deliberately avoids GORM's `AutoMigrate` feature for several critical reasons:

- **Explicit control**: Goose migrations provide a clear, versioned history of every schema change with timestamps and checksums, while `AutoMigrate` obscures when changes occur
- **Reversibility**: Goose requires `Down` migrations for rollbacks, whereas `AutoMigrate` cannot automatically reverse column drops or type changes
- **Safety**: `AutoMigrate` can drop columns or constraints unexpectedly if struct tags change, making it risky for production environments
- **Reviewability**: SQL migration files can be reviewed in pull requests by database administrators, while code-based auto-migration hides logic in compiled binaries

## Summary

- **Goose CLI** handles all schema migrations through versioned SQL files in `db/migrations/`, providing explicit, reversible database changes
- **GORM** manages database connections via `datastore.NewDB()` and provides the ORM layer for repository implementations in `pkg/adapter/repository/`
- **No runtime migrations** occur; the application assumes the schema already exists, enforced by the separation between `make migrate_schema_up` and application startup
- **Development workflow** involves creating Goose migration files, applying them via Make targets, then updating GORM models in `pkg/domain/model/` to match

## Frequently Asked Questions

### What is the difference between Goose and GORM AutoMigrate?

Goose is a database migration tool that uses versioned SQL files to explicitly define schema changes with `Up` and `Down` commands, while GORM `AutoMigrate` automatically creates or updates database tables based on Go struct definitions at runtime. The golang-clean-architecture repository uses Goose to ensure migrations are explicit, reversible, and reviewable, avoiding the risks of automatic schema modifications in production.

### How do I add a new column to the database in this architecture?

Create a new Goose migration file in `db/migrations/` with a timestamp prefix (e.g., [`20240115120000_add_column.sql`](https://github.com/manakuro/golang-clean-architecture/blob/main/20240115120000_add_column.sql)), write the `ALTER TABLE` statement in the `-- +goose Up` section and the corresponding rollback in `-- +goose Down`, then run `make migrate_schema_up`. Finally, update the corresponding GORM model in `pkg/domain/model/` to include the new field with appropriate tags.

### Is it safe to run migrations automatically when the application starts?

No, the repository explicitly avoids running migrations at application startup. The `NewUserRepository` function and other repository constructors expect the schema to already exist, and migrations must be applied manually via `make migrate_schema_up` or the Goose CLI before starting the application. This prevents race conditions, partial migrations, and accidental schema changes during deployments.

### How does the repository handle database seeding for development?

The repository includes a dedicated seeding mechanism separate from migrations. Running `make seed` executes a Go program (typically located in [`cmd/seed/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/seed/main.go)) that uses the GORM repository layer to insert sample data into the existing schema. This approach requires the schema to be migrated first via `make migrate_schema_up`, maintaining the separation between schema structure and data population.