Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions internal/app/di.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Container struct {
outboxRepo *outboxRepository.OutboxEventRepository

// Use Cases
userUseCase *userUsecase.UserUseCase
userUseCase userUsecase.UseCase

// Servers and Workers
httpServer *http.Server
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 2 additions & 11 deletions internal/user/http/user_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion internal/user/usecase/user_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion internal/user/usecase/user_usecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading