This document provides a comprehensive guide to creating standardized, modular components using the uber-go/fx framework, following the architectural principles outlined in project_structure.md. A consistent module structure is essential for building a scalable and maintainable application where dependencies are managed explicitly and lifecycles are handled gracefully.
- Self-Contained: Each module should be a self-contained unit of functionality. It defines its own configuration, dependencies, and constructor.
- Explicit Dependencies: All dependencies required by a module must be declared explicitly using the
fx.Instruct tag within aParamstruct. This makes the dependency graph clear and statically analyzable. - Interface-Driven: Modules that provide implementations (typically in
pkg) must provide them aslibinterfaces usingfx.As. This decouples the business logic from concrete implementations. - Lifecycle Aware: Modules that manage resources (like database connections or message queue consumers) must use
fx.Lifecycleto registerOnStartandOnStophooks for proper initialization and graceful shutdown.
The application is divided into distinct layers (pkg, internal/service, internal/controller), and each has a specific role within the Fx dependency injection framework.
The pkg layer's role is to provide a concrete, technology-specific implementation of an interface defined in the lib layer. It acts as a "Provider" of functionality to the rest of the application.
Key Characteristics:
- Implements a
libinterface. - Uses
fx.Providewithfx.Annotateandfx.Asto bind the concrete type to its interface. - Manages the lifecycle of the resource it provides (e.g., a database connection).
- Must not depend on other
libinterfaces.
Standard fx.go Template for a pkg Module:
// In pkg/client/postgres/fx.go
package postgres
import (
"context"
"fmt"
"os"
"strconv"
"go.uber.org/fx"
// Import the LIB interface that this module implements.
"your/project/lib/repository/user"
)
// The compile-time interface check is mandatory.
var _ user.Repository = (*UserRepository)(nil)
// Module exports the component's functionality to the Fx application.
// The module name (e.g., "postgres") should be descriptive and unique within the application.
var Module = fx.Module("postgres-user-repo",
// fx.Provide lists all the constructors this module offers to the DI container.
fx.Provide(
// The main constructor for the component.
// fx.Annotate is used to explicitly associate the concrete implementation
// with the interface it implements. This is a mandatory pattern.
fx.Annotate(
NewUserRepository, // The constructor function.
// fx.As casts the concrete return type (*UserRepository) to one or more
// interfaces (e.g., new(user.Repository)).
fx.As(new(user.Repository)),
),
// The constructor for the module's configuration.
ConfigRegister,
),
)
// Config holds the configuration specific to this module.
// These values MUST be populated from environment variables.
type Config struct {
DSN string
PoolSize int
}
// ConfigRegister loads and provides the module's configuration.
// It MUST enforce that required environment variables are present.
func ConfigRegister() (*Config, error) {
dsn := os.Getenv("POSTGRES_DSN")
if dsn == "" {
return nil, fmt.Errorf("POSTGRES_DSN environment variable is required")
}
poolSizeStr := os.Getenv("POSTGRES_POOL_SIZE")
if poolSizeStr == "" {
return nil, fmt.Errorf("POSTGRES_POOL_SIZE environment variable is required")
}
poolSize, err := strconv.Atoi(poolSizeStr)
if err != nil {
return nil, fmt.Errorf("invalid POSTGRES_POOL_SIZE: %w", err)
}
return &Config{
DSN: dsn,
PoolSize: poolSize,
}, nil
}
// Param is a struct that groups all dependencies for the main constructor.
// The `fx.In` tag tells Fx to populate the fields of this struct.
// This keeps the constructor signature clean and manageable.
type Param struct {
fx.In
// Lifecycle is a mandatory dependency for any module that manages a resource.
Lifecycle fx.Lifecycle
// The module's specific configuration.
Config *Config
// Other dependencies, such as a logger, can be added here.
// Logger client.Logger
}
// UserRepository is the concrete struct that implements the user.Repository interface.
type UserRepository struct {
// e.g., db *pgxpool.Pool
}
// NewUserRepository is the constructor for the UserRepository.
// It receives all its dependencies via the Param struct, provided by Fx.
func NewUserRepository(p Param) (*UserRepository, error) {
repo := &UserRepository{
// ... initialize fields ...
}
// The Fx lifecycle is used to register startup and shutdown hooks.
// This is mandatory for any resource that needs to be initialized or cleaned up.
p.Lifecycle.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
// Logic to run on application start.
// e.g., connect to the database using p.Config.DSN
// and assign the connection to repo.db.
return nil
},
OnStop: func(ctx context.Context) error {
// Logic to run on application stop.
// e.g., close the database connection.
return nil
},
})
return repo, nil
}The internal/service layer is where core business logic lives. Its role is to compose multiple lib interfaces from different pkg providers to orchestrate complex business workflows.
Key Characteristics:
- Depends only on the
libpackage (interfaces and data structures). - Does not depend on
internal/controllerorpkg. - Uses
fx.Provideto make the service available to thecontrollerlayer. - Typically does not manage resources directly, so
fx.Lifecycleis less common here.
Standard fx.go Template for a service Module:
// In internal/service/membership/fx.go
package membership
import (
"go.uber.org/fx"
// Import necessary lib interfaces.
"your/project/lib/adapter/notification"
"your/project/lib/logger"
"your/project/lib/repository/user"
)
var Module = fx.Module("membership-service",
fx.Provide(
NewService,
),
)
// Param struct for the service. It depends ONLY on interfaces from `lib`.
type Param struct {
fx.In
UserRepo user.Repository
Notifier notification.Adapter
Logger logger.Logger
}
// Service struct holds its dependencies, which are lib interfaces.
type Service struct {
userRepo user.Repository
notifier notification.Adapter
logger logger.Logger
}
// NewService is the constructor for the membership service.
func NewService(p Param) *Service {
return &Service{
userRepo: p.UserRepo,
notifier: p.Notifier,
logger: p.Logger,
}
}
// ... service methods that orchestrate business logic ...
// func (s *Service) RegisterUser(...) { ... }The internal/controller layer acts as the entry point for external requests (e.g., HTTP, gRPC). Its primary role is to handle incoming data, call the appropriate service methods, and formulate a response. It does not contain business logic.
Key Characteristics:
- Depends on
internal/servicestructs and thelibpackage. - Uses
fx.Invoketo register handlers (e.g., HTTP routes). Registration is a side effect, and the controller itself is not usually a dependency for other components, sofx.Invokeis preferred overfx.Provide. - The invoked function receives all dependencies needed to set up the handlers.
Standard fx.go Template for a controller Module:
// In internal/controller/userapi/fx.go
package userapi
import (
"net/http"
"github.com/go-chi/chi/v5" // Example using chi router
"go.uber.org/fx"
"your/project/lib/logger"
// Import the service it depends on.
"your/project/internal/service/membership"
)
// Module uses fx.Invoke because a controller's primary role is to register
// handlers, which is a side effect, not providing a new dependency.
var Module = fx.Module("userapi-controller",
fx.Invoke(RegisterRoutes),
)
// Param struct for the controller.
// It depends on services from `internal/service` and components via `lib` interfaces.
type Param struct {
fx.In
Router chi.Router // Assumes a chi.Router is provided by a pkg module.
MembershipSvc *membership.Service
Logger logger.Logger
}
// Controller holds dependencies needed by its handler methods.
type Controller struct {
service *membership.Service
logger logger.Logger
}
// RegisterRoutes creates the controller and sets up its HTTP routes.
func RegisterRoutes(p Param) {
c := &Controller{
service: p.MembershipSvc,
logger: p.Logger,
}
p.Router.Post("/users", c.CreateUser)
// ... other routes
}
// CreateUser is an example handler method.
func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
// 1. Decode request.
// 2. Call c.service to perform business logic.
// 3. Encode and write response.
}In addition to these standard templates, you can find detailed generation guides for specific technology stacks in the guide/packages/ subdirectory. These guides provide concrete, ready-to-use Fx module examples for common components like databases (Postgres, Redis), message queues (Kafka, NATS), and HTTP servers (Chi, Fiber).
When writing a new module, it is recommended to first check the guide/packages/ directory for a guide on a similar technology and use it as a baseline.
As described in entry_point.md, the cmd/.../app.go file is responsible for assembling all the application's modules. Fx will build the dependency graph, execute the constructors in the correct order, and run the application.
// In cmd/publicapi/app.go
package main
import (
"go.uber.org/fx"
// Import all necessary modules
"your/project/internal/controller/userapi"
"your/project/internal/service/membership"
"your/project/pkg/client/chi"
"your/project/pkg/client/postgres"
"your/project/pkg/client/zerolog"
)
func main() {
fx.New(
// List all modules here. Fx resolves the dependency order.
// pkg modules (Providers)
chi.Module,
postgres.Module,
zerolog.Module,
// service modules (Composers)
membership.Module,
// controller modules (Handlers)
userapi.Module,
).Run()
}Some pkg modules may require external command-line interface (CLI) tools for code generation (e.g., sqlc, templ). These tools are development-time dependencies and should not be part of the application's runtime.
Installation
These tools must be installed using the go install command to place the executable in your $GOPATH/bin directory. For example:
# Example for installing sqlc
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# Example for installing templ
go install github.com/a-h/templ/cmd/templ@latestDocumentation
If a pkg module requires such a tool, its installation command and usage must be clearly documented in its corresponding guide/packages/*.md file. This ensures that other developers can easily set up their environment to work with the module.