A thin, composable SQL layer built on top of gostratum/core providing GORM database integration with Fx-first composition, core/configx configuration, and core health check integration.
Several module options have been removed and moved to YAML config.
Removed Options:
- β
WithDefault(name)β Usedb.default: "primary"in YAML - β
WithRunMigrations()β Usedb.databases.<name>.auto_migrate: truein YAML - β
WithHealthChecks()β Usedb.enable_health_checks: truein YAML (default: true) - β
WithGolangMigrate()β Usedb.databases.<name>.auto_migrate: truein YAML - β
WithGolangMigrateEmbed()β Usedb.databases.<name>.migration_source: "embed://"in YAML - β
WithGolangMigrateDir(dir)β Usedb.databases.<name>.migration_source: "file://..."in YAML
Kept Options (Programmatic only):
- β
WithAutoMigrate(models ...any)- For Go types/models - β
WithMigrationsFS(fs embed.FS)- For embedded migrations - β
WithGormConfig(cfg *gorm.Config)- For complex GORM configuration
Migration Guide: See Phase 2 DBX Complete
- Fx-first composition - No globals, pure dependency injection
- Multi-database support - Manage multiple database connections
- Read replica support - Automatic read/write splitting for scalability
- Config-driven setup - Uses
core/configxfor configuration with sensible defaults - Auto-migration support - GORM model auto-migration and SQL file migrations
- Health integration - Automatic readiness/liveness checks via
core.Registry - Lifecycle management - Proper connection handling and graceful shutdown
- Observability-ready - Uses
core/logxfor structured logging - Transaction helpers - Simplified transaction management
- Connection pooling - Configurable connection pool settings
go get github.com/gostratum/dbxpackage main
import (
"context"
"github.com/gostratum/core"
"github.com/gostratum/dbx"
"go.uber.org/fx"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string
}
func main() {
app := core.New(
dbx.Module(
dbx.WithAutoMigrate(&User{}),
),
fx.Invoke(func(db *gorm.DB) {
// Use your database connection
var users []User
db.Find(&users)
}),
)
app.Run()
}Configuration (base.yaml or dev.yaml):
db:
default: primary
enable_health_checks: true
databases:
primary:
driver: postgres
dsn: "postgres://localhost:5432/mydb?sslmode=disable"
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 5m
auto_migrate: truepackage main
import (
"github.com/gin-gonic/gin"
"github.com/gostratum/core"
"github.com/gostratum/dbx"
"github.com/gostratum/httpx"
"go.uber.org/fx"
"gorm.io/gorm"
)
func main() {
app := core.New(
dbx.Module(
dbx.WithAutoMigrate(&User{}),
),
httpx.Module(),
fx.Invoke(func(e *gin.Engine, db *gorm.DB) {
e.GET("/api/users", func(c *gin.Context) {
var users []User
db.Find(&users)
c.JSON(200, users)
})
}),
)
app.Run()
}Configuration (base.yaml):
http:
addr: ":8080"
db:
default: primary
enable_health_checks: true
databases:
primary:
driver: postgres
dsn: "postgres://localhost:5432/mydb?sslmode=disable"The module uses core/configx for configuration loading. Configuration is automatically loaded from:
- Config files in the
./configsdirectory (or path specified byCONFIG_PATHSenv var) - Environment-specific files (e.g.,
dev.yaml,staging.yaml,prod.yamlbased onAPP_ENV) - Environment variables with
STRATUM_prefix
The configuration implements the configx.Configurable interface with the prefix "db".
The module uses the following default configuration:
db:
default: primary
databases:
primary:
driver: postgres
dsn: "postgres://localhost/dbname?sslmode=disable"
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 5m
conn_max_idle_time: 5m
log_level: warn
slow_threshold: 200ms
skip_default_tx: false
prepare_stmt: trueConfiguration can be overridden using environment variables with the STRATUM_ prefix:
# Example environment variables
STRATUM_DB_DEFAULT=primary
STRATUM_DB_DATABASES_PRIMARY_DRIVER=postgres
STRATUM_DB_DATABASES_PRIMARY_DSN="postgres://user:pass@localhost:5432/mydb?sslmode=disable"
STRATUM_DB_DATABASES_PRIMARY_MAX_OPEN_CONNS=50
STRATUM_DB_DATABASES_PRIMARY_MAX_IDLE_CONNS=10
STRATUM_DB_DATABASES_PRIMARY_LOG_LEVEL=infoNote: The STRATUM_ prefix is added by the core/configx loader. Nested keys use underscores (_) as separators.
db:
default: primary
databases:
primary:
driver: postgres
dsn: "postgres://localhost:5432/app_db?sslmode=disable"
max_open_conns: 25
analytics:
driver: postgres
dsn: "postgres://localhost:5432/analytics_db?sslmode=disable"
max_open_conns: 10
log_level: silentapp := core.New(
dbx.Module(dbx.WithDefault("primary")),
fx.Invoke(func(provider *dbx.Provider) {
primaryDB := provider.Get() // Gets default database
analyticsDB := provider.GetByName("analytics")
// Use different databases for different purposes
}),
)Configure read replicas for automatic read/write splitting:
db:
default: primary
databases:
primary:
driver: postgres
dsn: postgres://user:pass@primary:5432/db?sslmode=disable
# Read replicas for load balancing
read_replicas:
- postgres://user:pass@replica1:5432/db?sslmode=disable
- postgres://user:pass@replica2:5432/db?sslmode=disable
max_open_conns: 25Usage:
// Writes automatically go to primary
db.Create(&user) // β Primary
db.Update(&user) // β Primary
db.Delete(&user) // β Primary
// Reads can use replicas (automatic load balancing)
db.Find(&users) // β Replica (round-robin)
db.First(&user, 1) // β Replica
// Force primary database (for strong consistency after writes)
dbx.WithPrimary(db).First(&user, id) // β Primary
// Explicitly use replicas
dbx.WithReadReplicas(db).Find(&users) // β ReplicaSee: Read Replicas Example for complete setup with Docker Compose.
| Option | Description |
|---|---|
driver |
Database driver (currently supports: postgres) |
dsn |
Database connection string |
max_open_conns |
Maximum number of open connections |
max_idle_conns |
Maximum number of idle connections |
conn_max_lifetime |
Maximum lifetime of a connection |
conn_max_idle_time |
Maximum idle time of a connection |
log_level |
GORM log level (silent, error, warn, info) |
slow_threshold |
Threshold for slow query logging |
skip_default_tx |
Skip default transactions for performance |
prepare_stmt |
Enable prepared statements |
The DBX module uses a "Configuration for values, Options for code" pattern. Simple configuration values are managed via YAML config, while programmatic options handle Go types and complex objects.
Enables GORM auto-migration for specified models.
dbx.Module(
dbx.WithAutoMigrate(&User{}, &Product{}, &Order{}),
)Best for: Development and rapid prototyping
Sets embedded filesystem for SQL migrations.
//go:embed migrations/*.sql
var migrationsFS embed.FS
dbx.Module(
dbx.WithMigrationsFS(migrationsFS),
)Provides custom GORM configuration for advanced use cases.
dbx.Module(
dbx.WithGormConfig(&gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: true,
}),
)Simple configuration values are managed via YAML config instead of module options:
db:
default: primary # Default database connection name
enable_health_checks: true # Enable/disable health checks
databases:
primary:
driver: postgres # Database driver
dsn: "postgres://..." # Connection string
# Connection Pool
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 5m
conn_max_idle_time: 5m
# GORM Configuration
log_level: warn # GORM log level (silent, error, warn, info)
slow_threshold: 200ms # Threshold for slow query logging
skip_default_tx: false
prepare_stmt: true
# Migration Configuration
auto_migrate: false # Auto-migration (β οΈ dev only)
migration_source: "embed://" # Migration source (embed:// or file://path)
migration_table: "schema_migrations"
migration_lock_timeout: "15s"| Configuration | How to Set | Example |
|---|---|---|
| Default database | YAML: db.default |
db.default: "primary" |
| Health checks | YAML: db.enable_health_checks |
db.enable_health_checks: true |
| Auto-migration | YAML: db.databases.<name>.auto_migrate |
db.databases.primary.auto_migrate: true |
| Migration source | YAML: db.databases.<name>.migration_source |
db.databases.primary.migration_source: "embed://" |
| GORM models | Option: WithAutoMigrate() |
dbx.WithAutoMigrate(&User{}) |
| Embedded files | Option: WithMigrationsFS() |
dbx.WithMigrationsFS(embedFS) |
| GORM config | Option: WithGormConfig() |
dbx.WithGormConfig(cfg) |
The module automatically registers readiness and liveness health checks for all configured databases:
- Readiness checks: Simple database ping (3-second timeout)
- Liveness checks: Database ping + connection pool validation + test query (5-second timeout)
Health checks are registered with the core.Registry and can be accessed via standard health endpoints when using httpx module.
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myapp:latest
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 20GORM models are automatically migrated when the application starts:
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Email string `gorm:"uniqueIndex"`
CreatedAt time.Time
UpdatedAt time.Time
}
dbx.Module(
dbx.WithAutoMigrate(&User{}),
)Embed SQL migration files and run them at startup:
migrations/
βββ 001_create_indexes.sql
βββ 002_add_constraints.sql
βββ 003_seed_data.sql
//go:embed migrations/*.sql
var migrationsFS embed.FS
dbx.Module(
dbx.WithMigrationsFS(migrationsFS, "migrations"),
)Migration files are executed in alphabetical order and tracked to prevent re-execution.
func transferMoney(db *gorm.DB, fromID, toID uint, amount float64) error {
return dbx.WithTx(db, func(tx *gorm.DB) error {
// Debit from account
if err := tx.Model(&Account{}).Where("id = ?", fromID).
Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
return err
}
// Credit to account
return tx.Model(&Account{}).Where("id = ?", toID).
Update("balance", gorm.Expr("balance + ?", amount)).Error
})
}func processOrder(ctx context.Context, db *gorm.DB, order *Order) error {
return dbx.WithTxContext(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
// Create order
if err := tx.WithContext(ctx).Create(order).Error; err != nil {
return err
}
// Update inventory
return tx.WithContext(ctx).Model(&Product{}).
Where("id = ?", order.ProductID).
Update("stock", gorm.Expr("stock - ?", order.Quantity)).Error
})
}type OrderService struct {
txManager *dbx.TxManager
}
func (s *OrderService) ProcessOrder(order *Order) error {
return s.txManager.WithTx(func(tx *gorm.DB) error {
// Business logic with savepoints
if err := s.txManager.SavePoint(tx, "before_inventory"); err != nil {
return err
}
// Update inventory
if err := s.updateInventory(tx, order); err != nil {
// Rollback to savepoint on error
s.txManager.RollbackTo(tx, "before_inventory")
return err
}
return nil
})
}The module integrates with Zap logger and provides structured logging for all database operations:
// Automatic logging of:
// - SQL queries and execution time
// - Slow queries (configurable threshold)
// - Connection pool statistics
// - Transaction lifecycle
// - Migration progress
// Example log output:
{
"level": "info",
"time": "2023-01-01T12:00:00Z",
"caller": "dbx/logger.go:45",
"msg": "SQL query executed",
"elapsed": "15ms",
"sql": "SELECT * FROM users WHERE id = $1",
"rows": 1,
"trace_id": "abc123"
}fx.Invoke(func(provider *dbx.Provider) {
stats, err := provider.GetConnectionStats()
if err != nil {
return
}
for name, stat := range stats {
log.Printf("Database %s: %d/%d connections in use",
name, stat.InUse, stat.MaxOpenConnections)
}
})See example/main.go for a complete web application example with:
- Multiple models with auto-migration
- SQL file migrations
- REST API endpoints
- Health checks
- Transaction usage
package main
import (
"context"
"time"
"github.com/gostratum/core"
"github.com/gostratum/dbx"
"go.uber.org/fx"
"gorm.io/gorm"
)
type Job struct {
ID uint `gorm:"primaryKey"`
Status string
ProcessedAt *time.Time
}
func main() {
app := core.New(
dbx.Module(
dbx.WithDefault("primary"),
dbx.WithAutoMigrate(&Job{}),
),
fx.Invoke(func(lc fx.Lifecycle, db *gorm.DB) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go processJobs(ctx, db)
return nil
},
})
}),
)
app.Run()
}
func processJobs(ctx context.Context, db *gorm.DB) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
var jobs []Job
db.Where("status = ?", "pending").Find(&jobs)
for _, job := range jobs {
processJob(ctx, db, &job)
}
}
}
}package main
import (
"embed"
"log"
"github.com/gostratum/core"
"github.com/gostratum/dbx"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func main() {
app := core.New(
// Provide the embedded migrations FS and let the module/config drive migration behavior.
dbx.Module(
dbx.WithMigrationsFS(migrationsFS, "migrations"),
),
)
// Run the app (migrations will run depending on unified database configuration)
app.Run()
log.Println("Migrations completed (if enabled in configuration)")
}func TestConfigLoading(t *testing.T) {
// Create a test loader
loader, err := newConfigLoader(`
db:
default: test
databases:
test:
driver: postgres
dsn: "postgres://localhost/test"
`)
require.NoError(t, err)
cfg := &dbx.Config{}
err = loader.Bind(cfg)
assert.NoError(t, err)
assert.Equal(t, "test", cfg.Default)
assert.Equal(t, "postgres", cfg.Databases["test"].Driver)
}func TestHealthCheck(t *testing.T) {
// Mock database setup
db, mock := gormtest.NewMockDB()
connections := dbx.Connections{"test": db}
registry := core.NewHealthRegistry()
checker := dbx.NewHealthChecker(connections, registry)
mock.ExpectPing()
err := checker.RegisterHealthChecks()
assert.NoError(t, err)
// Verify checks are registered
result := registry.Aggregate(context.Background(), core.Readiness)
assert.Contains(t, result.Details, "db-test-readiness")
}- Go 1.25+
- PostgreSQL (for testing)
go test ./...cd example
go run main.goThe example server will start on :8080 with the following endpoints:
GET /api/health- Health checkGET /api/users- List usersPOST /api/users- Create userGET /api/products- List productsPOST /api/products- Create product
The dbx module now includes first-class support for database migrations using golang-migrate. This provides production-grade migration capabilities with support for both embedded and filesystem migrations.
- β First-class migration support using golang-migrate library
- β
Embedded migrations via
//go:embedfor easy deployment - β Filesystem migrations for development flexibility
- β CLI tool for manual migration management
- β Fx integration for automatic migrations on startup
- β Safe defaults - AutoMigrate disabled by default for production safety
- β Postgres native - Uses pgx driver for optimal performance
package main
import (
"github.com/gostratum/core"
"github.com/gostratum/dbx"
"go.uber.org/fx"
)
func main() {
app := core.New(
dbx.Module(
dbx.WithGolangMigrateEmbed(), // Use embedded migrations from dbx/migrate/files
),
fx.Invoke(func(db *gorm.DB) {
// Your database is ready with all migrations applied!
}),
)
app.Run()
}app := core.New(
dbx.Module(
dbx.WithGolangMigrateDir("./migrations"),
),
)Migration settings are now integrated directly into database configuration! This makes it much more intuitive - migration settings live alongside database settings.
db:
default: primary
databases:
primary:
# Database Connection
driver: postgres
dsn: postgres://user:pass@localhost:5432/mydb?sslmode=disable
max_open_conns: 25
log_level: info
# Migration Settings (integrated!)
migration_source: "embed://" # Use embedded migrations
auto_migrate: false # Safe default - NEVER enable in production!
migration_table: "schema_migrations" # Table for tracking migrations
migration_lock_timeout: "15s" # Lock timeout
migration_verbose: false # Quiet loggingpackage main
import (
"embed"
"github.com/gostratum/core"
"github.com/gostratum/dbx"
"go.uber.org/fx"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func main() {
app := core.New(
// Provide embedded migrations
fx.Provide(func() embed.FS { return migrationsFS }),
// DBX module - no migration options needed!
// Everything configured via unified database config
dbx.Module(),
fx.Invoke(func(db *gorm.DB) {
// Database ready with migrations applied!
}),
)
app.Run()
}# Database connection
DB_DATABASES_PRIMARY_DRIVER=postgres
DB_DATABASES_PRIMARY_DSN=postgres://user:pass@localhost:5432/mydb
# Migration settings (unified under database config)
DB_DATABASES_PRIMARY_MIGRATION_SOURCE=embed://
DB_DATABASES_PRIMARY_AUTO_MIGRATE=false
DB_DATABASES_PRIMARY_MIGRATION_VERBOSE=true| Format | Description | Use Case |
|---|---|---|
"embed://" |
Use embedded files (//go:embed) |
Production - files in binary |
"file://./migrations" |
Read from filesystem | Development - easy iteration |
"" (empty) |
No migrations | Cache/read-only databases |
Migration SQL files must be in YOUR application's directory:
your-app/
βββ main.go β Contains: //go:embed migrations/*.sql
βββ migrations/ β YOUR SQL FILES HERE
β βββ 000001_init.up.sql
β βββ 000001_init.down.sql
β βββ 000002_add_orders.up.sql
βββ go.mod
# Migration configuration using unified database environment variables
DB_DATABASES_PRIMARY_AUTO_MIGRATE=false
DB_DATABASES_PRIMARY_MIGRATION_SOURCE=embed://
DB_DATABASES_PRIMARY_MIGRATION_TABLE=schema_migrations
DB_DATABASES_PRIMARY_MIGRATION_LOCK_TIMEOUT=15s
DB_DATABASES_PRIMARY_MIGRATION_VERBOSE=true
### Creating Migrations
Migration files follow the naming convention: `{version}_{description}.{up|down}.sql`
#### Example Migration Files
**000001_init.up.sql**
```sql
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_users_email ON users(email);000001_init.down.sql
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;# Create a new migration
make migrate-create NAME=add_orders_table
# This creates:
# - migrate/files/000002_add_orders_table.up.sql
# - migrate/files/000002_add_orders_table.down.sqlUse the migration library programmatically:
package main
import (
"context"
"log"
"time"
"github.com/gostratum/dbx/migrate"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
dbURL := "postgres://user:pass@localhost:5432/dbname?sslmode=disable"
// Apply all pending migrations
if err := migrate.Up(ctx, dbURL, migrate.WithEmbed()); err != nil {
if migrate.IsNoChange(err) {
log.Println("No migrations to apply")
} else {
log.Fatal(err)
}
}
// Get migration status
status, err := migrate.GetStatus(ctx, dbURL, migrate.WithEmbed())
if err != nil {
log.Fatal(err)
}
log.Printf("Current version: %d, Dirty: %v", status.Current, status.Dirty)
// Migrate to specific version
if err := migrate.To(ctx, dbURL, 2, migrate.WithEmbed()); err != nil {
log.Fatal(err)
}
// Revert migrations
if err := migrate.Down(ctx, dbURL, migrate.WithEmbed()); err != nil {
log.Fatal(err)
}
}-
Never enable AutoMigrate in production
# β BAD - Automatic migrations in production dbx: migrate: auto_migrate: true # β GOOD - Manual migrations only dbx: migrate: auto_migrate: false
-
Use a migration review process
- All migrations should be code-reviewed
- Test migrations in staging first
- Have rollback plans ready
-
Run migrations before deployment
// In CI/CD pipeline, before deploying new code if err := migrate.Up(ctx, dbURL, migrate.WithEmbed()); err != nil { // Handle migration errors }
-
Monitor migration status
// Check migration status status, err := migrate.GetStatus(ctx, dbURL, migrate.WithEmbed()) if err != nil { // Handle error } log.Printf("Version: %d, Dirty: %v", status.Current, status.Dirty)
-
Handle migration failures gracefully
- Migrations are transactional (when possible)
- Use
forcecommand only for recovery - Always backup before major migrations
GitHub Actions Example:
name: Database Migrations
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run migrations
env:
DBX_URL: ${{ secrets.DATABASE_URL }}
run: |
go run main.go -migrate # or use your application's migration commandKubernetes Init Container:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
initContainers:
- name: migration
image: myapp:latest
command: ["/app/myapp", "-migrate"] # Use your app's migration command
env:
- name: DB_DATABASES_PRIMARY_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
containers:
- name: app
image: myapp:latestdbx/
βββ migrate/
β βββ files/ # Embedded migration files
β β βββ 000001_init.up.sql
β β βββ 000001_init.down.sql
β β βββ 000002_add_orders.up.sql
β β βββ 000002_add_orders.down.sql
β βββ config.go # Migration configuration
β βββ migrate.go # Public API
β βββ errors.go # Error types
β βββ internal/
β βββ runner.go # Migration runner
β βββ status.go # Status tracking
If a migration fails partway through, the state becomes "dirty":
// Check status
status, err := migrate.GetStatus(ctx, dbURL, migrate.WithEmbed())
if err != nil {
log.Fatal(err)
}
log.Printf("Current Version: %d, Dirty: %v", status.Current, status.Dirty)
// Fix the issue in your migration file, then force the version
if err := migrate.Force(ctx, dbURL, 1, migrate.WithEmbed()); err != nil {
log.Fatal(err)
}
// Re-run the migration
if err := migrate.Up(ctx, dbURL, migrate.WithEmbed()); err != nil {
log.Fatal(err)
}If migrations are locked (another process is running migrations):
# Increase lock timeout
dbx:
migrate:
lock_timeout: "30s" # Default is 15sEnsure your migration files are properly embedded:
// In dbx/migrate/source_embed.go
//go:embed files/*.sql
var EmbeddedMigrations embed.FS| Feature | GORM Auto-Migrate | golang-migrate |
|---|---|---|
| Control | Automatic schema sync | Explicit SQL migrations |
| Production Ready | β Safe | |
| Version Control | No | Yes |
| Rollback Support | No | Yes |
| Complex Changes | Limited | Full SQL power |
| Best For | Development, simple schemas | Production, complex schemas |
Recommendation: Use golang-migrate for production applications and complex schema changes. GORM auto-migrate can be used for rapid prototyping.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CHANGELOG.md for a complete list of changes.
This project is licensed under the MIT License - see the LICENSE file for details.
- Built on top of GORM - The fantastic Go ORM
- Inspired by the Gostratum framework philosophy
- Uses Uber Fx for dependency injection
Gostratum Philosophy: "Thin, composable building blocks for modern Go cloud applications β each module adds one capability cleanly layered on
core."