Grove
1.x
Docs/Grove/Multi-Module Migrations
Open

Reading2 min
Updated31 Jul 2026
Sourcev1/migrations/multi-module.mdx

Grove's migration system is designed for multi-module composition. Each Go module (including Forge extensions) can register its own migration group with declared dependencies.

Module-Owned Migration Groups01

Each module owns a named migration group:

// In package: github.com/xraph/forge-billing/migrations
var Migrations = migrate.NewGroup("forge.billing",
    migrate.DependsOn("core"),
)

func init() {
    Migrations.MustRegister(&migrate.Migration{
        Name:    "create_invoices",
        Version: "20240201000000",
        Up:      createInvoicesUp,
        Down:    createInvoicesDown,
    })
}

Dependency-Aware Ordering02

Migration groups declare dependencies that form a directed acyclic graph (DAG):

core                    (no dependencies)
├── forge.billing       (depends on: core)
├── forge.notifications (depends on: core)
└── forge.analytics     (depends on: core, forge.billing)

The migrator performs a topological sort to determine execution order:

  1. core migrations run first

  2. forge.billing and forge.notifications can run in any order

  3. forge.analytics runs after both core and forge.billing

Automatic Discovery03

When using Forge, migration groups are discovered automatically from registered extensions:

app := forge.New()
app.Use(billingExt.New())        // Registers forge.billing migrations
app.Use(notificationsExt.New())  // Registers forge.notifications migrations

// All migrations compose automatically at startup
app.Start(ctx)

Cycle Detection04

The migrator detects circular dependencies at startup:

// This would panic with ErrCyclicDependency
var A = migrate.NewGroup("a", migrate.DependsOn("b"))
var B = migrate.NewGroup("b", migrate.DependsOn("a"))

Advisory Locking05

To prevent concurrent migration runs (e.g., in a multi-replica deployment), the migrator acquires a database-level advisory lock:

  • PostgreSQL: pg_advisory_lock()

  • MySQL: GET_LOCK()

  • SQLite: File-based lock

If the lock cannot be acquired, the migrator returns ErrMigrationLocked.