diff --git a/README.md b/README.md index 30da703..8784c26 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ go-project-template/ The project follows a modular domain architecture where each business domain is organized in its own directory with clear separation of concerns: - **`domain/`** - Contains entities, value objects, and domain types (pure internal representation) -- **`usecase/`** - Implements business logic and orchestrates operations +- **`usecase/`** - Defines UseCase interfaces and implements business logic and orchestration - **`repository/`** - Handles data persistence and retrieval - **`http/`** - Contains HTTP handlers and DTOs (Data Transfer Objects) - **`dto/`** - Request/response DTOs and mappers (API contracts) @@ -380,7 +380,7 @@ The project follows a modular domain-driven structure where each business domain **User Domain** (`internal/user/`) - `domain/` - User entity and types -- `usecase/` - User registration, authentication logic +- `usecase/` - UseCase interface and user business logic implementation - `repository/` - User data persistence - `http/` - User HTTP endpoints and handlers - `dto/` - Request/response DTOs and mappers @@ -414,17 +414,17 @@ To add a new domain (e.g., `product`): ``` internal/product/ ├── domain/ -│ └── product.go # Domain entity (no JSON tags) +│ └── product.go # Domain entity (no JSON tags) ├── usecase/ -│ └── product_usecase.go # Business logic +│ └── product_usecase.go # UseCase interface + business logic ├── repository/ -│ └── product_repository.go # Data access +│ └── product_repository.go # Data access └── http/ ├── dto/ - │ ├── request.go # API request DTOs - │ ├── response.go # API response DTOs - │ └── mapper.go # DTO-to-domain mappers - └── product_handler.go # HTTP handlers + │ ├── request.go # API request DTOs + │ ├── response.go # API response DTOs + │ └── mapper.go # DTO-to-domain mappers + └── product_handler.go # HTTP handlers ``` #### 2. Register in DI container @@ -435,9 +435,9 @@ Add the new domain to the dependency injection container (`internal/app/di.go`): // Add fields to Container struct type Container struct { // ... existing fields - productRepo *productRepository.ProductRepository - productUseCase *productUsecase.ProductUseCase - productRepoInit sync.Once + productRepo *productRepository.ProductRepository + productUseCase productUsecase.UseCase // Interface, not concrete type + productRepoInit sync.Once productUseCaseInit sync.Once } @@ -454,6 +454,18 @@ func (c *Container) ProductRepository() (*productRepository.ProductRepository, e return c.productRepo, nil } +func (c *Container) ProductUseCase() (productUsecase.UseCase, error) { + var err error + c.productUseCaseInit.Do(func() { + c.productUseCase, err = c.initProductUseCase() + if err != nil { + c.initErrors["productUseCase"] = err + } + }) + // ... error handling + return c.productUseCase, nil +} + // Add initialization methods func (c *Container) initProductRepository() (*productRepository.ProductRepository, error) { db, err := c.DB() @@ -462,6 +474,20 @@ func (c *Container) initProductRepository() (*productRepository.ProductRepositor } return productRepository.NewProductRepository(db, c.config.DBDriver), nil } + +func (c *Container) initProductUseCase() (productUsecase.UseCase, error) { + txManager, err := c.TxManager() + if err != nil { + return nil, fmt.Errorf("failed to get tx manager: %w", err) + } + + productRepo, err := c.ProductRepository() + if err != nil { + return nil, fmt.Errorf("failed to get product repository: %w", err) + } + + return productUsecase.NewProductUseCase(txManager, productRepo) +} ``` #### 3. Wire handlers in HTTP server @@ -474,20 +500,24 @@ mux.HandleFunc("/api/products", productHandler.HandleProducts) ``` **Tips:** +- Define a UseCase interface in your usecase package to enable dependency inversion - Use the shared `httputil.MakeJSONResponse` function in your HTTP handlers for consistent JSON responses - Keep domain models free of JSON tags - use DTOs for API serialization - Implement validation in your request DTOs - Create mapper functions to convert between DTOs and domain models - Register all components in the DI container for proper lifecycle management +- HTTP handlers should depend on the UseCase interface, not concrete implementations ### Clean Architecture Layers 1. **Domain Layer** - Contains business entities and rules (e.g., `internal/user/domain`) 2. **Repository Layer** - Data access implementations using sqlutil (e.g., `internal/user/repository`) -3. **Use Case Layer** - Application business logic (e.g., `internal/user/usecase`) +3. **Use Case Layer** - UseCase interfaces and application business logic (e.g., `internal/user/usecase`) 4. **Presentation Layer** - HTTP handlers and server (e.g., `internal/user/http`) 5. **Utility Layer** - Shared utilities and helpers (e.g., `internal/httputil`) +**Dependency Inversion Principle:** The presentation layer (HTTP handlers) and infrastructure (DI container) depend on UseCase interfaces defined in the usecase layer, not on concrete implementations. This enables better testability and decoupling. + ### Data Transfer Objects (DTOs) The project enforces clear boundaries between internal domain models and external API contracts using DTOs: diff --git a/internal/app/di.go b/internal/app/di.go index d9f573c..2999e75 100644 --- a/internal/app/di.go +++ b/internal/app/di.go @@ -36,7 +36,7 @@ type Container struct { outboxRepo *outboxRepository.OutboxEventRepository // Use Cases - userUseCase *userUsecase.UserUseCase + userUseCase userUsecase.UseCase // Servers and Workers httpServer *http.Server @@ -152,7 +152,7 @@ func (c *Container) OutboxRepository() (*outboxRepository.OutboxEventRepository, } // UserUseCase returns the user use case instance. -func (c *Container) UserUseCase() (*userUsecase.UserUseCase, error) { +func (c *Container) UserUseCase() (userUsecase.UseCase, error) { var err error c.userUseCaseInit.Do(func() { c.userUseCase, err = c.initUserUseCase() @@ -301,7 +301,7 @@ func (c *Container) initOutboxRepository() (*outboxRepository.OutboxEventReposit } // initUserUseCase creates the user use case with all its dependencies. -func (c *Container) initUserUseCase() (*userUsecase.UserUseCase, error) { +func (c *Container) initUserUseCase() (userUsecase.UseCase, error) { txManager, err := c.TxManager() if err != nil { return nil, fmt.Errorf("failed to get tx manager for user use case: %w", err) diff --git a/internal/http/server.go b/internal/http/server.go index 20c812c..e6f128c 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -24,7 +24,7 @@ func NewServer( host string, port int, logger *slog.Logger, - userUseCaseInstance *userUsecase.UserUseCase, + userUseCaseInstance userUsecase.UseCase, ) *Server { userHandler := userHttp.NewUserHandler(userUseCaseInstance, logger) diff --git a/internal/user/http/user_handler.go b/internal/user/http/user_handler.go index c6dfc5b..b691db0 100644 --- a/internal/user/http/user_handler.go +++ b/internal/user/http/user_handler.go @@ -2,32 +2,23 @@ package http import ( - "context" "encoding/json" "log/slog" "net/http" "github.com/allisson/go-project-template/internal/httputil" - "github.com/allisson/go-project-template/internal/user/domain" "github.com/allisson/go-project-template/internal/user/http/dto" "github.com/allisson/go-project-template/internal/user/usecase" ) -// UserUseCaseInterface defines the interface for user use case operations -type UserUseCaseInterface interface { - RegisterUser(ctx context.Context, input usecase.RegisterUserInput) (*domain.User, error) - GetUserByEmail(ctx context.Context, email string) (*domain.User, error) - GetUserByID(ctx context.Context, id int64) (*domain.User, error) -} - // UserHandler handles user-related HTTP requests type UserHandler struct { - userUseCase UserUseCaseInterface + userUseCase usecase.UseCase logger *slog.Logger } // NewUserHandler creates a new UserHandler -func NewUserHandler(userUseCase UserUseCaseInterface, logger *slog.Logger) *UserHandler { +func NewUserHandler(userUseCase usecase.UseCase, logger *slog.Logger) *UserHandler { return &UserHandler{ userUseCase: userUseCase, logger: logger, diff --git a/internal/user/usecase/user_usecase.go b/internal/user/usecase/user_usecase.go index 91bbccc..79a6429 100644 --- a/internal/user/usecase/user_usecase.go +++ b/internal/user/usecase/user_usecase.go @@ -19,6 +19,13 @@ type RegisterUserInput struct { Password string `json:"password"` } +// UseCase defines the interface for user business logic operations +type UseCase interface { + RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) + GetUserByEmail(ctx context.Context, email string) (*domain.User, error) + GetUserByID(ctx context.Context, id int64) (*domain.User, error) +} + // UserRepository interface defines user repository operations type UserRepository interface { Create(ctx context.Context, user *domain.User) error @@ -46,7 +53,7 @@ func NewUserUseCase( txManager database.TxManager, userRepo UserRepository, outboxRepo OutboxEventRepository, -) (*UserUseCase, error) { +) (UseCase, error) { // Initialize password hasher with interactive policy for user passwords hasher, err := pwdhash.New(pwdhash.WithPolicy(pwdhash.PolicyInteractive)) if err != nil { diff --git a/internal/user/usecase/user_usecase_test.go b/internal/user/usecase/user_usecase_test.go index 53a5ed2..6f316f7 100644 --- a/internal/user/usecase/user_usecase_test.go +++ b/internal/user/usecase/user_usecase_test.go @@ -88,7 +88,6 @@ func TestNewUserUseCase(t *testing.T) { useCase, err := NewUserUseCase(txManager, userRepo, outboxRepo) require.NoError(t, err) assert.NotNil(t, useCase) - assert.NotNil(t, useCase.passwordHasher) } func TestUserUseCase_RegisterUser_Success(t *testing.T) {