🛠️ Building Modular Go Services with Uber FX and SQLC
In the journey toward scalable, maintainable Go applications, few things matter more than clean architecture, lifecycle clarity, and domain-driven modularity. go-simple project exemplifies this evolution, transforming a basic Gin + Uber FX setup into a robust, idiomatic service with SQLC-backed persistence and domain-centric layering.
This article walks through the architectural decisions and implementation strategies that shaped the product module in, based on the commit.
In the previous article, we converted the entire project to Uber FX, thereby improving its modularity and making dependency management more systematic.
🧱 Domain-Centric Structure: Why It Matters
Rather than scattering controllers, services, and repositories across layer-centric folders, Mobin chose a domain-centric layout:
internal/
└── product/
├── controller/
├── service/
├── repository/
└── module.goThis structure encapsulates all business logic product in one place, making it easier to scale, test, and onboard new contributors.
⚙️ SQLC Integration: Declarative, Type-Safe Persistence
SQLC was introduced to generate type-safe Go code from raw SQL queries. The schema and query files live under:
internal/db/sqlc/
├── schema/
│
└── schema.sql
├── queries/
│
└── product.sqlThe schema defines the products table, while the queries cover full CRUD operations. SQLC generates Go bindings that are injected into the repository layer.
🚀 Lifecycle-Driven Migrations
To ensure the database schema is applied before the server starts, Mobin integrated golang-migrate into the FX lifecycle:
fx.Invoke(migrate.RunMigrations)This guarantees that migrations run automatically on startup, keeping the database in sync with the codebase.
🔌 Final Composition in main.go
The NewApp() function brings it all together:
func NewApp() *fx.App {
return fx.New(fx.Provide(config.NewConfig, health.New, server.NewGinEngine,
server.CreateHTTPServer, migrate.NewRunner, // ← migration runner),
fx.Invoke(server.RegisterRoutes, server.StartHTTPServer, migrate.RunMigrations, // ← migration
hook),)
}This setup ensures that configuration, migrations, server startup, and domain modules are all orchestrated predictably.
Docker compose file
services:
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
postgres:
image: postgres:latest
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=database
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stoppedvolumes:
redis-data:
postgres-data:🧠 Lessons from the Refactor
- Domain-centric design improves modularity and clarity.
- Uber FX simplifies dependency injection and lifecycle management.
- SQLC brings type safety and performance to database access.
- Docker + Postgres + Redis provide a clean dev environment.
- Migrations in FX ensure schema consistency without manual steps.
🧩 Conclusion
The evolution of go-simple from a basic Gin server into a modular, lifecycle-driven architecture reflects a deeper commitment to clarity, reliability, and idiomatic Go design. By embracing domain-centric structure, SQLC for type-safe persistence, and Uber FX for dependency injection and lifecycle orchestration.
This refactor isn’t just about cleaner code — it’s about building systems that think in terms of domains, evolve gracefully, and stay predictable under pressure. Whether you’re adding new modules, integrating middleware, or preparing for production, the patterns established here offer a blueprint for thoughtful Go development.
As go-simple continues to grow, it stands as a testament to architectural discipline and the power of iterative refinement. The journey from simplicity to elegance has only just begun.
Your Business — On AutoPilot with DDImedia AI Assistant
(Join Our Waitlist)
Visit us at DataDrivenInvestor.com
Join our creator ecosystem here.
DDI Official Telegram Channel: https://t.me/+tafUp6ecEys4YjQ1
