🧱 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:

go
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 product
  • PUT /admin/products/:id → Update product
  • DELETE /admin/products/:id → Delete product
  • GET /admin/products/:id → Get product by ID
  • GET /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:

go
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:

go
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:

go
// @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 zap and 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.