š§± Building Modular Product Management in Go with SQLC, Uber FX, andĀ Gin
In a recent commit to go-simple, a robust and idiomatic product management module that exemplifies clean architecture, modularity, and lifecycle-aware service design in Go. This article walks through the key decisions and implementation patterns that shaped the commit, and reflects on the architectural philosophy behind it.
In the previous session, we created a database. In this session, we plan to develop both a controller and a service for managing products, which will extend the functionality of the application.
š§© Domain-Centric ServiceĀ Layer
At the heart of the module is a product.Service that wraps SQLC-generated queries. Rather than exposing raw database logic, Mobin designed a clean API that accepts DTOs and returns domain-specific responses:
func (s *Service) Create(ctx context.Context, req dto.AdminCreateProductRequest)
(dto.ProductResponse, error
)
func (s *Service) Update(ctx context.Context, req dto.AdminUpdateProductRequest)
(dto.ProductResponse, error
)
func (s *Service) Delete(ctx context.Context, id int32) errorfunc (s *Service) GetProductByID(ctx
context.Context, id int32) (dto.ProductResponse, error
)
func (s *Service) ListProducts(ctx context.Context) ([]dto.ProductResponse, error)This approach encapsulates SQLCās CreateProduct, UpdateProduct, and ListProducts methods, while translating them into clean, predictable DTOs. It also avoids unnecessary interfaces, favoring direct composition for simplicity and performance.
š Gin-Based Admin Controller
To expose the service via HTTP, Mobin implemented a Gin controller with full CRUD support:
POST /admin/productsā Create productPUT /admin/products/:idā Update productDELETE /admin/products/:idā Delete productGET /admin/products/:idā Get product by IDGET /admin/productsā List all products
Each handler uses ShouldBindJSON, parses path parameters, and delegates to the service layer. Errors are handled via a centralized response.JSONError helper, which logs structured JSON using zap:
func JSONError(ctx *gin.Context, status int, err error, logger *zap.Logger) {
logger.Error("request error", zap.Int("status", status), zap.String("error", err.Error()),
zap.String("path", ctx.Request.URL.Path), zap.String("method", ctx.Request.Method),
)
ctx.AbortWithStatusJSON(status, ErrorResponse{
Error: err.Error()
})
}This ensures consistent error responses and observability across the API.
š§ SQLC Integration withĀ DBTX
The service layer relies on SQLCās Queries struct, which expects a DBTX interface. Mobin resolved this via Uber FX by providing a *sql.DB instance:
fx.Provide(func(cfg *config.Config) (*sql.DB, error) { return sql.Open("postgres",
cfg.DatabaseURL)})This satisfies the DBTX dependency and allows SQLC to operate seamlessly within the FX lifecycle.
š Swagger Documentation
Mobin also added Swagger annotations to document the API. For example, the GetProductByID endpoint includes:
// @Summary Get a product by ID// @Description Retrieve a single product using its unique ID// @Tags
Products// @Param id path int true "Product ID"// @Success 200 {object} dto.ProductResponse//
@Failure 400 {object} response.ErrorResponse// @Failure 500 {object} response.ErrorResponse//
@Router /products/{id} [get]To ensure Swagger parses the DTOs correctly, Mobin added dummy references and imported the dto package explicitly. Fields in ProductResponse were exported and tagged with json and example annotations for clarity.
š§ Architectural Philosophy
This commit reflects Mobinās evolving architectural mindset:
- Domain-centric modularity: Each feature lives in its own folder with DTOs, service, and controller.
- Lifecycle-aware design: Uber FX wires dependencies cleanly and supports migration hooks.
- Idiomatic Go: Avoids unnecessary interfaces, favors composition, and uses clear naming.
- Observability: Structured logging with
zapand consistent error handling. - Scalability: Ready for client/admin separation, Swagger docs, and test harnesses.
š§© Conclusion
Mobinās work on the go-simple project showcases a thoughtful blend of pragmatism and architectural clarity. By grounding the product module in SQLC, Uber FX, and Gin, heās built a foundation thatās not only idiomatic but also scalable, testable, and maintainable. The decision to avoid unnecessary interfaces, embrace domain-centric DTOs, and centralize error handling with structured logging reflects a deep understanding of Goās strengths and the realities of production-grade development.
This commit isnāt just about adding a featureāāāitās about codifying a philosophy: that reliability, clarity, and modularity are not trade-offs, but pillars of sustainable software. Whether youāre building a microservice or a monolith, Mobinās approach offers a blueprint for clean, composable Go systems that evolve gracefully with your team and your codebase.
Let me know if youād like to turn this into a publishable blog post or internal architecture docāāāitās a great example of how thoughtful design leads to resilient systems.
