diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a156124
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,426 @@
+# Agent Guidelines for Go Project Template
+
+This document provides guidelines for AI coding agents working in this Go project. Follow these conventions to maintain consistency with the existing codebase.
+
+## Build, Lint, and Test Commands
+
+### Building
+```bash
+make build # Build the application binary
+make run-server # Build and run HTTP server
+make run-worker # Build and run background worker
+make run-migrate # Build and run database migrations
+```
+
+### Testing
+```bash
+make test # Run all tests with coverage
+make test-with-db # Start databases, run tests, stop databases (full cycle)
+make test-db-up # Start PostgreSQL (5433) and MySQL (3307) test databases
+make test-db-down # Stop test databases
+
+# Run a single test or test file
+go test -v ./internal/user/repository -run TestPostgreSQLUserRepository_Create
+go test -v ./internal/user/usecase -run TestUserUseCase
+
+# Run tests for a specific package
+go test -v ./internal/user/repository
+go test -v ./internal/user/usecase
+
+# Run tests with race detection
+go test -v -race ./...
+
+# View coverage in browser
+make test-coverage
+```
+
+### Linting
+```bash
+make lint # Run golangci-lint with auto-fix
+golangci-lint run -v --fix # Direct linter command
+```
+
+### Other Commands
+```bash
+make clean # Remove build artifacts and coverage files
+make deps # Download and tidy dependencies
+make help # Show all available make targets
+```
+
+## Project Architecture
+
+This is a **Clean Architecture** project following **Domain-Driven Design** principles with a modular domain structure.
+
+### Directory Structure
+- `cmd/app/` - Application entry point (CLI with server/worker/migrate commands)
+- `internal/`
+ - `{domain}/` - Domain modules (e.g., `user/`, `outbox/`)
+ - `domain/` - Entities, value objects, domain errors
+ - `usecase/` - UseCase interfaces and business logic
+ - `repository/` - Data persistence (MySQL and PostgreSQL implementations)
+ - `http/` - HTTP handlers and DTOs
+ - `dto/` - Request/response DTOs, mappers
+ - `app/` - Dependency injection container
+ - `errors/` - Standardized domain errors
+ - `httputil/` - HTTP utilities (JSON responses, error handling)
+ - `database/` - Database connection and transaction management
+ - `config/` - Configuration management
+ - `validation/` - Custom validation rules
+ - `testutil/` - Test helper utilities
+
+### Layer Responsibilities
+
+**Domain Layer** (`domain/`)
+- Define entities with pure business logic (no JSON tags)
+- Define domain-specific errors by wrapping standard errors from `internal/errors`
+- Example: `ErrUserNotFound = errors.Wrap(errors.ErrNotFound, "user not found")`
+
+**Use Case Layer** (`usecase/`)
+- Define UseCase interfaces for dependency inversion
+- Implement business logic and orchestration
+- Validate input using `github.com/jellydator/validation`
+- Return domain errors directly without additional wrapping
+- Manage transactions using `TxManager.WithTx()`
+
+**Repository Layer** (`repository/`)
+- Implement separate repositories for MySQL and PostgreSQL
+- Transform infrastructure errors to domain errors (e.g., `sql.ErrNoRows` → `domain.ErrUserNotFound`)
+- Use `database.GetTx(ctx, r.db)` to support transactions
+- Check for unique constraint violations and return appropriate domain errors
+
+**HTTP Layer** (`http/`)
+- Define request/response DTOs with JSON tags (keep domain models pure)
+- Validate DTOs using `jellydator/validation`
+- Use `httputil.HandleError()` for automatic error-to-HTTP status mapping
+- Use `httputil.MakeJSONResponse()` for consistent JSON responses
+- Depend on UseCase interfaces, not concrete implementations
+
+## Code Style Guidelines
+
+### Imports
+Follow this import grouping order (enforced by `goimports`):
+1. Standard library packages
+2. External packages (third-party)
+3. Local project packages (prefixed with `github.com/allisson/go-project-template`)
+
+Example:
+```go
+import (
+ "context"
+ "database/sql"
+ "strings"
+
+ "github.com/google/uuid"
+ validation "github.com/jellydator/validation"
+
+ "github.com/allisson/go-project-template/internal/database"
+ "github.com/allisson/go-project-template/internal/errors"
+ "github.com/allisson/go-project-template/internal/user/domain"
+)
+```
+
+**Important:** When renaming imports, use descriptive aliases:
+- `apperrors` for `internal/errors`
+- `appValidation` for `internal/validation`
+- `outboxDomain` for `internal/outbox/domain`
+
+### Formatting
+- **Line length**: Max 110 characters (enforced by `golines`)
+- **Indentation**: Use tabs (tab-len: 4)
+- **Comments**: Not shortened by golines
+- Use `goimports` and `golines` for consistent formatting
+- Run `make lint` before committing
+
+### Naming Conventions
+
+**Packages**
+- Use lowercase, single-word names when possible
+- Avoid underscores or mixed caps
+- Examples: `domain`, `usecase`, `repository`, `http`
+
+**Types**
+- Use PascalCase for exported types
+- Use camelCase for unexported types
+- Suffix interfaces with meaningful names, not just "Interface"
+ - Good: `UserRepository`, `TxManager`, `UseCase`
+ - Bad: `UserRepositoryInterface`, `IUserRepository`
+
+**Functions/Methods**
+- Use PascalCase for exported functions
+- Use camelCase for unexported functions
+- Prefix boolean functions with `is`, `has`, `can`, or `should`
+ - Examples: `isPostgreSQLUniqueViolation`, `hasUpperCase`
+
+**Variables**
+- Use camelCase for short-lived variables
+- Use descriptive names for longer-lived variables
+- Avoid single-letter names except for: `i`, `j`, `k` (loops), `r` (receiver), `w` (http.ResponseWriter), `ctx` (context)
+
+**Constants**
+- Use PascalCase for exported constants
+- Use camelCase for unexported constants
+
+### Types and Interfaces
+
+**UUIDs**
+- Use `uuid.UUID` type from `github.com/google/uuid`
+- Generate IDs with `uuid.Must(uuid.NewV7())` (time-ordered UUIDs)
+- PostgreSQL: Store as native `UUID` type
+- MySQL: Store as `BINARY(16)` with marshal/unmarshal
+
+**Domain Models**
+- Keep domain models pure (no JSON tags)
+- Use DTOs for API serialization
+- Example:
+ ```go
+ // Domain model (no JSON tags)
+ type User struct {
+ ID uuid.UUID
+ Name string
+ Email string
+ Password string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ }
+ ```
+
+**DTOs**
+- Add JSON tags for API contracts
+- Implement `Validate()` method using `jellydator/validation`
+- Create mapper functions to convert between DTOs and domain models
+
+### Error Handling
+
+**Standard Domain Errors** (from `internal/errors`)
+- `ErrNotFound` - Resource doesn't exist (404)
+- `ErrConflict` - Duplicate or conflicting data (409)
+- `ErrInvalidInput` - Validation failures (422)
+- `ErrUnauthorized` - Authentication required (401)
+- `ErrForbidden` - Permission denied (403)
+
+**Domain-Specific Errors**
+Always wrap standard errors with domain context:
+```go
+var (
+ ErrUserNotFound = errors.Wrap(errors.ErrNotFound, "user not found")
+ ErrUserAlreadyExists = errors.Wrap(errors.ErrConflict, "user already exists")
+ ErrInvalidEmail = errors.Wrap(errors.ErrInvalidInput, "invalid email format")
+)
+```
+
+**Error Flow**
+1. **Repository Layer**: Transform infrastructure errors to domain errors
+ ```go
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, domain.ErrUserNotFound
+ }
+ ```
+
+2. **Use Case Layer**: Return domain errors directly (don't wrap again)
+ ```go
+ user, err := uc.userRepo.GetByID(ctx, id)
+ if err != nil {
+ return nil, err // Pass through domain error
+ }
+ ```
+
+3. **HTTP Handler Layer**: Use `httputil.HandleError()` for automatic mapping
+ ```go
+ if err != nil {
+ httputil.HandleError(w, err, h.logger)
+ return
+ }
+ ```
+
+**Error Checking**
+- Use `errors.Is()` to check for specific errors
+- Use `errors.As()` to extract error types
+- Never compare errors with `==` (except for `sql.ErrNoRows` from stdlib)
+
+### Validation
+
+Use `github.com/jellydator/validation` for input validation:
+
+```go
+import (
+ validation "github.com/jellydator/validation"
+ appValidation "github.com/allisson/go-project-template/internal/validation"
+)
+
+func (r *RegisterUserRequest) Validate() error {
+ err := validation.ValidateStruct(r,
+ validation.Field(&r.Name,
+ validation.Required.Error("name is required"),
+ appValidation.NotBlank,
+ validation.Length(1, 255),
+ ),
+ validation.Field(&r.Email,
+ validation.Required.Error("email is required"),
+ appValidation.Email,
+ ),
+ validation.Field(&r.Password,
+ validation.Required.Error("password is required"),
+ appValidation.PasswordStrength{
+ MinLength: 8,
+ RequireUpper: true,
+ RequireLower: true,
+ RequireNumber: true,
+ RequireSpecial: true,
+ },
+ ),
+ )
+ return appValidation.WrapValidationError(err)
+}
+```
+
+**Custom Validation Rules** (in `internal/validation`):
+- `Email` - Email format validation
+- `NotBlank` - Not empty after trimming
+- `NoWhitespace` - No leading/trailing whitespace
+- `PasswordStrength` - Configurable password requirements
+
+### Database Patterns
+
+**Repository Pattern**
+- Implement separate repositories for MySQL and PostgreSQL
+- Use `database.GetTx(ctx, r.db)` to get querier (works with and without transactions)
+- Use numbered placeholders for PostgreSQL (`$1, $2`) and `?` for MySQL
+
+**Transaction Management**
+```go
+err := uc.txManager.WithTx(ctx, func(ctx context.Context) error {
+ if err := uc.userRepo.Create(ctx, user); err != nil {
+ return err
+ }
+ if err := uc.outboxRepo.Create(ctx, event); err != nil {
+ return err
+ }
+ return nil
+})
+```
+
+**Unique Constraint Violations**
+Check for database-specific error patterns:
+- PostgreSQL: `strings.Contains(err.Error(), "duplicate key")`
+- MySQL: `strings.Contains(err.Error(), "duplicate entry")`
+
+### Testing
+
+**Integration Testing Philosophy**
+- Use real databases (PostgreSQL port 5433, MySQL port 3307) instead of mocks
+- Tests verify actual SQL queries and database behavior
+- Start test databases with `make test-db-up` before running tests
+
+**Test Structure**
+```go
+func TestPostgreSQLUserRepository_Create(t *testing.T) {
+ db := testutil.SetupPostgresDB(t) // Connect and run migrations
+ defer testutil.TeardownDB(t, db) // Clean up connection
+ defer testutil.CleanupPostgresDB(t, db) // Clean up test data
+
+ repo := NewPostgreSQLUserRepository(db)
+ ctx := context.Background()
+
+ user := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: "John Doe",
+ Email: "john@example.com",
+ Password: "hashed_password",
+ }
+
+ err := repo.Create(ctx, user)
+ assert.NoError(t, err)
+}
+```
+
+**Test Naming**
+- Format: `Test{Type}_{Method}` or `Test{Type}_{Method}_{Scenario}`
+- Examples: `TestPostgreSQLUserRepository_Create`, `TestUserUseCase_RegisterUser_DuplicateEmail`
+
+### Comments and Documentation
+
+**Package Comments**
+Every package should have a doc comment:
+```go
+// Package usecase implements the user business logic and orchestrates user domain operations.
+package usecase
+```
+
+**Exported Types**
+Document all exported types, functions, and constants:
+```go
+// User represents a user in the system
+type User struct { ... }
+
+// RegisterUser registers a new user and creates a user.created event
+func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
+```
+
+**Implementation Comments**
+Add comments for complex logic:
+```go
+// Hash the password using Argon2id
+hashedPassword, err := uc.passwordHasher.Hash([]byte(input.Password))
+```
+
+## Adding New Domains
+
+When adding a new domain (e.g., `product`):
+
+1. **Create the domain structure**:
+ ```
+ internal/product/
+ ├── domain/
+ │ └── product.go # Entity + domain errors
+ ├── usecase/
+ │ └── product_usecase.go # UseCase interface + implementation
+ ├── repository/
+ │ ├── mysql_product_repository.go
+ │ └── postgresql_product_repository.go
+ └── http/
+ ├── dto/
+ │ ├── request.go # With Validate() method
+ │ ├── response.go # With JSON tags
+ │ └── mapper.go # DTO ↔ domain conversions
+ └── product_handler.go # Uses httputil functions
+ ```
+
+2. **Define domain errors** in `domain/product.go`:
+ ```go
+ var (
+ ErrProductNotFound = errors.Wrap(errors.ErrNotFound, "product not found")
+ ErrInvalidPrice = errors.Wrap(errors.ErrInvalidInput, "invalid price")
+ )
+ ```
+
+3. **Register in DI container** (`internal/app/di.go`):
+ - Add repository and use case fields
+ - Add getter methods with `sync.Once` initialization
+ - Select repository based on `c.config.DBDriver`
+
+4. **Wire HTTP handlers** in `internal/http/server.go`
+
+5. **Write migrations** for both PostgreSQL and MySQL
+
+## Common Pitfalls to Avoid
+
+❌ **Don't** add JSON tags to domain models
+✅ **Do** use DTOs for API serialization
+
+❌ **Don't** wrap domain errors multiple times
+✅ **Do** return domain errors directly from use cases
+
+❌ **Don't** use `sql.ErrNoRows` in use cases
+✅ **Do** transform to domain errors in repositories
+
+❌ **Don't** use single repository for both databases
+✅ **Do** implement separate MySQL and PostgreSQL repositories
+
+❌ **Don't** use auto-incrementing integer IDs
+✅ **Do** use UUIDv7 with `uuid.Must(uuid.NewV7())`
+
+❌ **Don't** create mocks for repositories
+✅ **Do** use real test databases (ports 5433 and 3307)
+
+❌ **Don't** depend on concrete use case implementations
+✅ **Do** depend on UseCase interfaces in handlers and DI container
diff --git a/README.md b/README.md
index 866485b..f9237fb 100644
--- a/README.md
+++ b/README.md
@@ -1,329 +1,144 @@
-# Go Project Template
-
-A production-ready Go project template following Clean Architecture and Domain-Driven Design principles, optimized for building scalable applications with PostgreSQL or MySQL.
-
-## Features
-
-- **Modular Domain Architecture** - Domain-based code organization for scalability
-- **Clean Architecture** - Separation of concerns with domain, repository, use case, and presentation layers
-- **Standardized Error Handling** - Domain errors with proper HTTP status code mapping
-- **Dependency Injection Container** - Centralized component wiring with lazy initialization and clean resource management
-- **Multiple Database Support** - PostgreSQL and MySQL with dedicated repository implementations using `database/sql`
-- **Database Migrations** - Separate migrations for PostgreSQL and MySQL using golang-migrate
-- **UUIDv7 Primary Keys** - Time-ordered, sortable UUIDs for globally unique identifiers
-- **Transaction Management** - TxManager interface for handling database transactions
-- **Transactional Outbox Pattern** - Event-driven architecture with guaranteed delivery
-- **HTTP Server** - Standard library HTTP server with middleware for logging and panic recovery
-- **Worker Process** - Background worker for processing outbox events
-- **CLI Interface** - urfave/cli for running server, migrations, and worker
-- **Health Checks** - Kubernetes-compatible readiness and liveness endpoints
-- **Structured Logging** - JSON logs using slog
-- **Configuration** - Environment variable based configuration with go-env
-- **Input Validation** - Advanced validation with jellydator/validation library including password strength, email format, and custom rules
-- **Password Hashing** - Secure password hashing with Argon2id via go-pwdhash
-- **Docker Support** - Multi-stage Dockerfile for minimal container size
-- **Integration Testing** - Real database tests using Docker Compose instead of mocks
-- **CI/CD** - GitHub Actions workflow with PostgreSQL and MySQL for comprehensive testing
-- **Comprehensive Makefile** - Easy development and deployment commands
-
-## Project Structure
+# 🚀 Go Project Template
-```
-go-project-template/
-├── cmd/
-│ └── app/ # Application entry point
-│ └── main.go
-├── internal/
-│ ├── app/ # Dependency injection container
-│ │ ├── di.go
-│ │ ├── di_test.go
-│ │ └── README.md
-│ ├── config/ # Configuration management
-│ │ └── config.go
-│ ├── database/ # Database connection and transaction management
-│ │ ├── database.go
-│ │ └── txmanager.go
-│ ├── errors/ # Standardized domain errors
-│ │ └── errors.go
-│ ├── http/ # HTTP server and shared infrastructure
-│ │ ├── middleware.go
-│ │ ├── response.go
-│ │ └── server.go
-│ ├── httputil/ # HTTP utility functions
-│ │ └── response.go # JSON responses and error mapping
-│ ├── outbox/ # Outbox domain module
-│ │ ├── domain/ # Outbox entities
-│ │ │ └── outbox_event.go
-│ │ └── repository/ # Outbox data access
-│ │ ├── mysql_outbox_repository.go
-│ │ └── postgresql_outbox_repository.go
-│ ├── user/ # User domain module
-│ │ ├── domain/ # User entities and domain errors
-│ │ │ └── user.go
-│ │ ├── http/ # User HTTP handlers
-│ │ │ ├── dto/ # Request/response DTOs
-│ │ │ │ ├── request.go
-│ │ │ │ ├── response.go
-│ │ │ │ └── mapper.go
-│ │ │ └── user_handler.go
-│ │ ├── repository/ # User data access
-│ │ │ ├── mysql_user_repository.go
-│ │ │ └── postgresql_user_repository.go
-│ │ └── usecase/ # User business logic
-│ │ └── user_usecase.go
-│ ├── validation/ # Custom validation rules
-│ │ ├── rules.go
-│ │ └── rules_test.go
-│ ├── testutil/ # Test utilities
-│ │ └── database.go # Database test helpers
-│ └── worker/ # Background workers
-│ └── event_worker.go
-├── migrations/
-│ ├── mysql/ # MySQL migrations
-│ └── postgresql/ # PostgreSQL migrations
-├── .github/
-│ └── workflows/
-│ └── ci.yml # CI workflow with PostgreSQL & MySQL
-├── docker-compose.test.yml # Test database configuration
-├── Dockerfile
-├── Makefile
-├── go.mod
-└── go.sum
-```
+> A production-ready Go project template following Clean Architecture and Domain-Driven Design principles, optimized for building scalable applications with PostgreSQL or MySQL.
-### Domain Module Structure
+[](https://github.com/allisson/go-project-template/actions)
+[](https://goreportcard.com/report/github.com/allisson/go-project-template)
+[](https://opensource.org/licenses/MIT)
-The project follows a modular domain architecture where each business domain is organized in its own directory with clear separation of concerns:
+## ✨ Features
-- **`domain/`** - Contains entities, value objects, domain types, and domain-specific errors
-- **`usecase/`** - Defines UseCase interfaces and implements business logic and orchestration
-- **`repository/`** - Handles data persistence and retrieval, transforms infrastructure errors to domain errors
-- **`http/`** - Contains HTTP handlers and DTOs (Data Transfer Objects)
- - **`dto/`** - Request/response DTOs and mappers (API contracts)
+- 🏗️ **Clean Architecture** - Clear separation of concerns with domain, repository, use case, and presentation layers
+- 📦 **Modular Domain Structure** - Easy to add new domains without affecting existing code
+- 🔌 **Dependency Injection** - Centralized component wiring with lazy initialization
+- 🗄️ **Multi-Database Support** - PostgreSQL and MySQL with dedicated repository implementations
+- 🔄 **Database Migrations** - Separate migrations for PostgreSQL and MySQL
+- 🆔 **UUIDv7 Primary Keys** - Time-ordered, globally unique identifiers
+- 💼 **Transaction Management** - Built-in support for database transactions
+- 📬 **Transactional Outbox Pattern** - Event-driven architecture with guaranteed delivery
+- ⚠️ **Standardized Error Handling** - Domain errors with proper HTTP status code mapping
+- ✅ **Input Validation** - Advanced validation with custom rules (email, password strength, etc.)
+- 🔒 **Password Hashing** - Secure Argon2id password hashing
+- 🧪 **Integration Testing** - Real database tests instead of mocks
+- 🐳 **Docker Support** - Multi-stage Dockerfile and Docker Compose setup
+- 🚦 **CI/CD Ready** - GitHub Actions workflow with comprehensive testing
+- 📊 **Structured Logging** - JSON logs using slog
+- 🛠️ **Comprehensive Makefile** - Easy development and deployment commands
-### Shared Utilities
+## 📚 Documentation
-- **`app/`** - Dependency injection container for assembling application components
-- **`errors/`** - Standardized domain errors for expressing business intent
-- **`httputil/`** - Shared HTTP utility functions including error mapping and JSON responses
-- **`config/`** - Application-wide configuration
-- **`database/`** - Database connection and transaction management
-- **`testutil/`** - Test helper utilities for database setup and cleanup
-- **`worker/`** - Background processing infrastructure
+- 📖 [Getting Started](docs/getting-started.md) - Installation and setup guide
+- 🏗️ [Architecture](docs/architecture.md) - Architectural patterns and design principles
+- 🛠️ [Development](docs/development.md) - Development workflow and coding standards
+- 🧪 [Testing](docs/testing.md) - Testing strategies and best practices
+- ⚠️ [Error Handling](docs/error-handling.md) - Error handling system guide
+- ➕ [Adding Domains](docs/adding-domains.md) - Step-by-step guide to add new domains
-This structure makes it easy to add new domains (e.g., `internal/product/`, `internal/order/`) without affecting existing modules.
+## 🚀 Quick Start
-## Prerequisites
+### Prerequisites
-- Go 1.25 or higher
-- PostgreSQL 12+ or MySQL 8.0+ (for development)
-- Docker and Docker Compose (for testing and optional development)
-- Make (optional, for convenience commands)
+- Go 1.25+
+- PostgreSQL 12+ or MySQL 8.0+
+- Docker and Docker Compose (optional)
-## Quick Start
-
-### 1. Clone the repository
+### Installation
```bash
+# Clone the repository
git clone https://github.com/allisson/go-project-template.git
cd go-project-template
-```
-
-### 2. Customize the module path
-
-After cloning, you need to update the import paths to match your project:
-
-**Option 1: Using find and sed (Linux/macOS)**
-```bash
-# Replace with your actual module path
-NEW_MODULE="github.com/yourname/yourproject"
-
-# Update go.mod
-sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" go.mod
-
-# Update all Go files
-find . -type f -name "*.go" -exec sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" {} +
-```
-
-**Option 2: Using PowerShell (Windows)**
-```powershell
-# Replace with your actual module path
-$NEW_MODULE = "github.com/yourname/yourproject"
-
-# Update go.mod
-(Get-Content go.mod) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content go.mod
-# Update all Go files
-Get-ChildItem -Recurse -Filter *.go | ForEach-Object {
- (Get-Content $_.FullName) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content $_.FullName
-}
-```
-
-**Option 3: Manually**
-1. Update the module name in `go.mod`
-2. Search and replace `github.com/allisson/go-project-template` with your module path in all `.go` files
-
-After updating, verify the changes and tidy dependencies:
-```bash
-go mod tidy
-```
-
-**Important:** Also update the `.golangci.yml` file to match your new module path:
-
-```yaml
-formatters:
- settings:
- goimports:
- local-prefixes:
- - github.com/yourname/yourproject # Update this line
-```
-
-This ensures the linter correctly groups your local imports.
-
-### UUIDv7 Primary Keys
-
-The project uses **UUIDv7** for all primary keys instead of auto-incrementing integers. UUIDv7 provides several advantages:
-
-**Benefits:**
-- **Time-ordered**: UUIDs include timestamp information, maintaining temporal ordering
-- **Globally unique**: No collision risk across distributed systems or databases
-- **Database friendly**: Better index performance than random UUIDs (v4) due to sequential nature
-- **Scalability**: No need for centralized ID generation or coordination
-- **Merge-friendly**: Databases can be merged without ID conflicts
+# Install dependencies
+go mod download
-**Implementation:**
+# Start a database (using Docker)
+make dev-postgres # or make dev-mysql
-All ID fields use `uuid.UUID` type from `github.com/google/uuid`:
+# Run migrations
+make run-migrate
-```go
-import "github.com/google/uuid"
-
-type User struct {
- ID uuid.UUID `db:"id" json:"id"`
- Name string `db:"name"`
- Email string `db:"email"`
- CreatedAt time.Time `db:"created_at"`
- UpdatedAt time.Time `db:"updated_at"`
-}
+# Start the server
+make run-server
```
-IDs are generated in the application code using `uuid.NewV7()`:
+The server will be available at http://localhost:8080
-```go
-user := &domain.User{
- ID: uuid.Must(uuid.NewV7()),
- Name: input.Name,
- Email: input.Email,
- Password: hashedPassword,
-}
-```
+For detailed setup instructions, see the [Getting Started Guide](docs/getting-started.md).
-**Database Storage:**
-- **PostgreSQL**: `UUID` type (native support)
-- **MySQL**: `BINARY(16)` type (16-byte storage)
-
-**Migration Example (PostgreSQL):**
-```sql
-CREATE TABLE users (
- id UUID PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- email VARCHAR(255) UNIQUE NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-```
+## 📖 Project Structure
-**Migration Example (MySQL):**
-```sql
-CREATE TABLE users (
- id BINARY(16) PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- email VARCHAR(255) UNIQUE NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
-
-### 3. Install dependencies
-
-```bash
-go mod download
+go-project-template/
+├── cmd/app/ # Application entry point
+├── internal/
+│ ├── app/ # Dependency injection container
+│ ├── config/ # Configuration management
+│ ├── database/ # Database connection and transactions
+│ ├── errors/ # Standardized domain errors
+│ ├── http/ # HTTP server infrastructure
+│ ├── httputil/ # HTTP utilities (JSON responses, error mapping)
+│ ├── validation/ # Custom validation rules
+│ ├── testutil/ # Test utilities
+│ ├── worker/ # Background workers
+│ ├── user/ # User domain module
+│ │ ├── domain/ # User entities and domain errors
+│ │ ├── usecase/ # User business logic
+│ │ ├── repository/ # User data access
+│ │ └── http/ # User HTTP handlers and DTOs
+│ └── outbox/ # Outbox domain module
+├── migrations/
+│ ├── postgresql/ # PostgreSQL migrations
+│ └── mysql/ # MySQL migrations
+├── docs/ # Documentation
+├── Dockerfile
+├── Makefile
+└── docker-compose.test.yml
```
-### 4. Configure environment variables
+Learn more about the architecture in the [Architecture Guide](docs/architecture.md).
-The application automatically loads environment variables from a `.env` file. Create a `.env` file in your project root (or any parent directory):
+## 🧪 Testing
```bash
-# Database configuration
-DB_DRIVER=postgres # or mysql
-DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
-DB_MAX_OPEN_CONNECTIONS=25
-DB_MAX_IDLE_CONNECTIONS=5
-DB_CONN_MAX_LIFETIME=5
-
-# Server configuration
-SERVER_HOST=0.0.0.0
-SERVER_PORT=8080
-
-# Logging
-LOG_LEVEL=info
-
-# Worker configuration
-WORKER_INTERVAL=5
-WORKER_BATCH_SIZE=10
-WORKER_MAX_RETRIES=3
-WORKER_RETRY_INTERVAL=1
-```
-
-**Note:** The application searches for the `.env` file recursively from the current working directory up to the root directory. This allows you to run the application from any subdirectory and it will still find your `.env` file.
-
-Alternatively, you can export environment variables directly without a `.env` file.
-
-### 5. Start a database (using Docker)
+# Start test databases
+make test-db-up
-**PostgreSQL:**
-```bash
-make dev-postgres
-```
+# Run all tests
+make test
-**MySQL:**
-```bash
-make dev-mysql
-```
+# Run tests with coverage
+make test-coverage
-### 6. Run database migrations
+# Stop test databases
+make test-db-down
-```bash
-make run-migrate
+# Or run everything in one command
+make test-with-db
```
-### 7. Start the HTTP server
+The project uses real PostgreSQL and MySQL databases for testing instead of mocks. See the [Testing Guide](docs/testing.md) for details.
-```bash
-make run-server
-```
+## 🛠️ Development
-The server will be available at http://localhost:8080
-
-### 8. Start the worker (in another terminal)
+### Build Commands
```bash
-make run-worker
+make build # Build the application
+make run-server # Run HTTP server
+make run-worker # Run background worker
+make run-migrate # Run database migrations
+make lint # Run linter with auto-fix
+make clean # Clean build artifacts
```
-## Usage
-
-### HTTP Endpoints
+### API Endpoints
#### Health Check
```bash
curl http://localhost:8080/health
```
-#### Readiness Check
-```bash
-curl http://localhost:8080/ready
-```
-
#### Register User
```bash
curl -X POST http://localhost:8080/api/users \
@@ -335,992 +150,140 @@ curl -X POST http://localhost:8080/api/users \
}'
```
-**Password Requirements:**
-- Minimum 8 characters
-- At least one uppercase letter
-- At least one lowercase letter
-- At least one number
-- At least one special character
-
-**Validation Errors:**
-
-If validation fails, you'll receive a 422 Unprocessable Entity response with details:
-
-```json
-{
- "error": "invalid_input",
- "message": "email: must be a valid email address; password: password must contain at least one uppercase letter."
-}
-```
-
-### CLI Commands
+For more development workflows, see the [Development Guide](docs/development.md).
-The binary supports three commands via urfave/cli:
+## 🔑 Key Concepts
-#### Start HTTP Server
-```bash
-./bin/app server
-```
-
-#### Run Database Migrations
-```bash
-./bin/app migrate
-```
-
-#### Run Event Worker
-```bash
-./bin/app worker
-```
-
-## Error Handling
-
-The project implements a standardized error handling system that expresses business intent rather than exposing infrastructure details.
-
-### Domain Error Architecture
-
-**Standard Domain Errors** (`internal/errors/errors.go`)
-
-The project defines standard domain errors that can be used across all modules:
-
-```go
-var (
- ErrNotFound = errors.New("not found") // 404 Not Found
- ErrConflict = errors.New("conflict") // 409 Conflict
- ErrInvalidInput = errors.New("invalid input") // 422 Unprocessable Entity
- ErrUnauthorized = errors.New("unauthorized") // 401 Unauthorized
- ErrForbidden = errors.New("forbidden") // 403 Forbidden
-)
-```
-
-**Domain-Specific Errors** (`internal/user/domain/user.go`)
-
-Each domain defines its own specific errors that wrap the standard errors:
-
-```go
-var (
- ErrUserNotFound = errors.Wrap(errors.ErrNotFound, "user not found")
- ErrUserAlreadyExists = errors.Wrap(errors.ErrConflict, "user already exists")
- ErrInvalidEmail = errors.Wrap(errors.ErrInvalidInput, "invalid email format")
-)
-```
+### Clean Architecture Layers
-### Error Flow Through Layers
+- **Domain Layer** 🎯 - Business entities and rules (pure, no external dependencies)
+- **Repository Layer** 💾 - Data persistence (separate MySQL and PostgreSQL implementations)
+- **Use Case Layer** 💼 - Business logic and orchestration
+- **Presentation Layer** 🌐 - HTTP handlers and DTOs
+- **Utility Layer** 🛠️ - Shared utilities (error handling, validation, HTTP helpers)
-**1. Repository Layer** - Transforms infrastructure errors to domain errors:
+### Error Handling System
-```go
-func (r *PostgreSQLUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
- querier := database.GetTx(ctx, r.db)
- query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1`
-
- var user domain.User
- err := querier.QueryRowContext(ctx, query, id).Scan(
- &user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt,
- )
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return nil, domain.ErrUserNotFound // Infrastructure → Domain
- }
- return nil, apperrors.Wrap(err, "failed to get user by id")
- }
- return &user, nil
-}
-```
+The project uses a standardized error handling system:
-**2. Use Case Layer** - Returns domain errors directly:
+- **Standard Errors**: `ErrNotFound`, `ErrConflict`, `ErrInvalidInput`, `ErrUnauthorized`, `ErrForbidden`
+- **Domain Errors**: Wrap standard errors with domain-specific context
+- **Automatic HTTP Mapping**: Domain errors automatically map to appropriate HTTP status codes
+Example:
```go
-func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
- if strings.TrimSpace(input.Email) == "" {
- return nil, domain.ErrEmailRequired // Domain error
- }
-
- if err := uc.userRepo.Create(ctx, user); err != nil {
- return nil, err // Pass through domain errors
- }
- return user, nil
-}
-```
-
-**3. HTTP Handler Layer** - Maps domain errors to HTTP responses:
+// Define domain error
+var ErrUserNotFound = errors.Wrap(errors.ErrNotFound, "user not found")
-```go
-func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
- user, err := h.userUseCase.RegisterUser(r.Context(), input)
- if err != nil {
- httputil.HandleError(w, err, h.logger) // Auto-maps to HTTP status
- return
- }
- httputil.MakeJSONResponse(w, http.StatusCreated, response)
+// Use in repository
+if errors.Is(err, sql.ErrNoRows) {
+ return nil, domain.ErrUserNotFound // Maps to 404 Not Found
}
```
-### HTTP Error Responses
-
-The `httputil.HandleError` function automatically maps domain errors to appropriate HTTP status codes:
-
-| Domain Error | HTTP Status | Error Code | Example Response |
-|--------------|-------------|------------|------------------|
-| `ErrNotFound` | 404 | `not_found` | `{"error":"not_found","message":"The requested resource was not found"}` |
-| `ErrConflict` | 409 | `conflict` | `{"error":"conflict","message":"A conflict occurred with existing data"}` |
-| `ErrInvalidInput` | 422 | `invalid_input` | `{"error":"invalid_input","message":"invalid email format"}` |
-| `ErrUnauthorized` | 401 | `unauthorized` | `{"error":"unauthorized","message":"Authentication is required"}` |
-| `ErrForbidden` | 403 | `forbidden` | `{"error":"forbidden","message":"You don't have permission"}` |
-| Unknown | 500 | `internal_error` | `{"error":"internal_error","message":"An internal error occurred"}` |
-
-### Benefits
-
-1. **No Infrastructure Leaks** - Database errors are never exposed to API clients
-2. **Business Intent** - Errors express domain concepts (`ErrUserNotFound` vs `sql.ErrNoRows`)
-3. **Consistent HTTP Mapping** - Same domain error always maps to same HTTP status
-4. **Type-Safe** - Use `errors.Is()` to check for specific error types
-5. **Structured Responses** - All errors return consistent JSON format
-6. **Centralized Logging** - All errors are logged with full context before responding
-
-### Adding Errors to New Domains
-
-When creating a new domain, define domain-specific errors:
-
-```go
-// internal/product/domain/product.go
-var (
- ErrProductNotFound = errors.Wrap(errors.ErrNotFound, "product not found")
- ErrInsufficientStock = errors.Wrap(errors.ErrConflict, "insufficient stock")
- ErrInvalidPrice = errors.Wrap(errors.ErrInvalidInput, "invalid price")
-)
-```
-
-Then use `httputil.HandleError()` in your HTTP handlers for automatic mapping.
+Learn more in the [Error Handling Guide](docs/error-handling.md).
-## Development
-
-### Build the application
-
-```bash
-make build
-```
-
-### Testing
-
-**Start test databases:**
-```bash
-make test-db-up
-```
-
-**Run tests:**
-```bash
-make test
-```
-
-**Run tests with automatic database management:**
-```bash
-make test-with-db # Starts databases, runs tests, stops databases
-```
-
-**Run tests with coverage:**
-```bash
-make test-coverage
-```
-
-**Stop test databases:**
-```bash
-make test-db-down
-```
-
-See the [Testing](#testing) section for more details.
-
-### Run linter
-
-```bash
-make lint
-```
+### UUIDv7 Primary Keys
-### Clean build artifacts
+All entities use UUIDv7 for primary keys:
+- ⏱️ Time-ordered for better database performance
+- 🌍 Globally unique across distributed systems
+- 📊 Better than UUIDv4 for database indexes
-```bash
-make clean
-```
+### Transactional Outbox Pattern
-## Docker
+Ensures reliable event delivery:
+1. Business operation and event stored in same transaction
+2. Background worker processes pending events
+3. Guarantees at-least-once delivery
-### Build Docker image
+## 🐳 Docker
```bash
+# Build Docker image
make docker-build
-```
-
-### Run server in Docker
-```bash
+# Run server in Docker
make docker-run-server
-```
-
-### Run worker in Docker
-```bash
+# Run worker in Docker
make docker-run-worker
-```
-
-### Run migrations in Docker
-```bash
+# Run migrations in Docker
make docker-run-migrate
```
-## Architecture
-
-### Database Repository Pattern
+## 🔧 Configuration
-The project uses separate repository implementations for MySQL and PostgreSQL, leveraging Go's standard `database/sql` package. This approach provides:
+All configuration is done via environment variables. Create a `.env` file in your project root:
-- **Database-specific optimizations** - Each implementation is tailored to the specific database's features and syntax
-- **Type safety** - Direct use of database/sql types without abstraction layers
-- **Clarity** - Explicit SQL queries make it clear what operations are being performed
-- **No external dependencies** - Uses only the standard library for database operations
-
-**Repository Interface** (defined in use case layer):
-
-```go
-// internal/user/usecase/user_usecase.go
-type UserRepository interface {
- Create(ctx context.Context, user *domain.User) error
- GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error)
- GetByEmail(ctx context.Context, email string) (*domain.User, error)
-}
-```
-
-**MySQL Implementation** (`internal/user/repository/mysql_user_repository.go`):
-
-```go
-func (r *MySQLUserRepository) Create(ctx context.Context, user *domain.User) error {
- querier := database.GetTx(ctx, r.db)
-
- query := `INSERT INTO users (id, name, email, password, created_at, updated_at)
- VALUES (?, ?, ?, ?, NOW(), NOW())`
-
- // Convert UUID to bytes for MySQL BINARY(16)
- uuidBytes, err := user.ID.MarshalBinary()
- if err != nil {
- return apperrors.Wrap(err, "failed to marshal UUID")
- }
-
- _, err = querier.ExecContext(ctx, query, uuidBytes, user.Name, user.Email, user.Password)
- if err != nil {
- if isMySQLUniqueViolation(err) {
- return domain.ErrUserAlreadyExists
- }
- return apperrors.Wrap(err, "failed to create user")
- }
- return nil
-}
-```
-
-**PostgreSQL Implementation** (`internal/user/repository/postgresql_user_repository.go`):
-
-```go
-func (r *PostgreSQLUserRepository) Create(ctx context.Context, user *domain.User) error {
- querier := database.GetTx(ctx, r.db)
-
- query := `INSERT INTO users (id, name, email, password, created_at, updated_at)
- VALUES ($1, $2, $3, $4, NOW(), NOW())`
-
- _, err := querier.ExecContext(ctx, query, user.ID, user.Name, user.Email, user.Password)
- if err != nil {
- if isPostgreSQLUniqueViolation(err) {
- return domain.ErrUserAlreadyExists
- }
- return apperrors.Wrap(err, "failed to create user")
- }
- return nil
-}
-```
-
-**Key Differences:**
-
-| Feature | MySQL | PostgreSQL |
-|---------|-------|------------|
-| UUID Storage | `BINARY(16)` - requires marshaling/unmarshaling | Native `UUID` type |
-| Placeholders | `?` for all parameters | `$1, $2, $3...` numbered parameters |
-| Unique Errors | Check for "1062" or "duplicate entry" | Check for "duplicate key" or "unique constraint" |
-
-**Transaction Support:**
-
-The `database.GetTx()` helper function retrieves the transaction from context if available, otherwise returns the DB connection:
-
-```go
-// internal/database/txmanager.go
-type Querier interface {
- ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
- QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
- QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
-}
-
-func GetTx(ctx context.Context, db *sql.DB) Querier {
- if tx, ok := ctx.Value(txKey{}).(*sql.Tx); ok {
- return tx
- }
- return db
-}
-```
-
-This pattern ensures repositories work seamlessly within transactions managed by the use case layer.
-
-### Testing Approach
-
-The project uses **integration testing with real databases** instead of mocks for repository layer tests. This approach provides:
-
-- **Accuracy** - Tests verify actual SQL queries and database behavior
-- **Real Integration** - Catches database-specific issues (constraints, types, unique violations, etc.)
-- **Production Parity** - Tests reflect real production scenarios
-- **Less Maintenance** - No mock expectations to maintain or update
-- **Confidence** - Full database integration coverage
-
-**Test Infrastructure:**
-
-Tests use Docker Compose to spin up isolated test databases (PostgreSQL on port 5433, MySQL on port 3307) with dedicated test credentials. The `testutil` package provides helper functions that:
-
-1. Connect to test databases
-2. Run migrations automatically
-3. Clean up data between tests
-4. Provide isolated test environments
-
-**Example test structure:**
-
-```go
-func TestPostgreSQLUserRepository_Create(t *testing.T) {
- db := testutil.SetupPostgresDB(t) // Connect and run migrations
- defer testutil.TeardownDB(t, db) // Clean up connection
- defer testutil.CleanupPostgresDB(t, db) // Clean up test data
-
- repo := NewPostgreSQLUserRepository(db)
- ctx := context.Background()
-
- user := &domain.User{
- ID: uuid.Must(uuid.NewV7()),
- Name: "John Doe",
- Email: "john@example.com",
- Password: "hashed_password",
- }
-
- err := repo.Create(ctx, user)
- assert.NoError(t, err)
-
- // Verify by querying the real database
- createdUser, err := repo.GetByID(ctx, user.ID)
- assert.NoError(t, err)
- assert.Equal(t, user.Name, createdUser.Name)
-}
-```
-
-This testing strategy ensures repository implementations work correctly with actual databases while maintaining fast test execution (~15 seconds for the full test suite).
-
-### Dependency Injection Container
-
-The project uses a custom dependency injection (DI) container located in `internal/app/` to manage all application components. This provides:
-
-- **Centralized component wiring** - All dependencies are assembled in one place
-- **Lazy initialization** - Components are only created when first accessed
-- **Singleton pattern** - Each component is initialized once and reused
-- **Clean resource management** - Unified shutdown for all resources
-- **Thread-safe** - Safe for concurrent access across goroutines
-
-**Example usage:**
-
-```go
-// Create container with configuration
-container := app.NewContainer(cfg)
-
-// Get HTTP server (automatically initializes all dependencies)
-server, err := container.HTTPServer()
-if err != nil {
- return fmt.Errorf("failed to initialize HTTP server: %w", err)
-}
-
-// Clean shutdown
-defer container.Shutdown(ctx)
-```
-
-The container manages the entire dependency graph:
-
-```
-Container
-├── Infrastructure (Database, Logger)
-├── Repositories (User, Outbox)
-├── Use Cases (User)
-└── Presentation (HTTP Server, Worker)
-```
-
-For more details on the DI container, see [`internal/app/README.md`](internal/app/README.md).
-
-### Modular Domain Architecture
-
-The project follows a modular domain-driven structure where each business domain is self-contained:
-
-**User Domain** (`internal/user/`)
-- `domain/` - User entity and types
-- `usecase/` - UseCase interface and user business logic implementation
-- `repository/` - User data persistence
-- `http/` - User HTTP endpoints and handlers
- - `dto/` - Request/response DTOs and mappers
-
-**Outbox Domain** (`internal/outbox/`)
-- `domain/` - OutboxEvent entity and status types
-- `repository/` - Event persistence and retrieval
-
-**Shared Infrastructure**
-- `app/` - Dependency injection container for component assembly
-- `config/` - Application configuration
-- `database/` - Database connection and transaction management
-- `errors/` - Standardized domain errors (ErrNotFound, ErrConflict, etc.)
-- `http/` - HTTP server, middleware, and shared utilities
-- `httputil/` - Reusable HTTP utilities (JSON responses, error handling, status code mapping)
-- `worker/` - Background event processing
-
-### Benefits of This Structure
-
-1. **Scalability** - Easy to add new domains without affecting existing code
-2. **Encapsulation** - Each domain is self-contained with clear boundaries
-3. **Team Collaboration** - Teams can work on different domains independently
-4. **Maintainability** - Related code is co-located, making it easier to understand and modify
-5. **Dependency Management** - Centralized DI container simplifies component wiring and testing
-
-### Adding New Domains
-
-To add a new domain (e.g., `product`):
-
-#### 1. Create the domain structure
-
-```
-internal/product/
-├── domain/
-│ └── product.go # Domain entity + domain errors
-├── usecase/
-│ └── product_usecase.go # UseCase interface + business logic
-├── repository/
-│ ├── mysql_product_repository.go # MySQL data access
-│ └── postgresql_product_repository.go # PostgreSQL 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 (uses httputil.HandleError)
-```
-
-**Define domain errors in your entity file:**
-
-```go
-// internal/product/domain/product.go
-package domain
-
-import (
- "time"
- apperrors "github.com/yourname/yourproject/internal/errors"
- "github.com/google/uuid"
-)
-
-type Product struct {
- ID uuid.UUID
- Name string
- Price float64
- Stock int
- CreatedAt time.Time
- UpdatedAt time.Time
-}
-
-// Domain-specific errors
-var (
- ErrProductNotFound = apperrors.Wrap(apperrors.ErrNotFound, "product not found")
- ErrInsufficientStock = apperrors.Wrap(apperrors.ErrConflict, "insufficient stock")
- ErrInvalidPrice = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid price")
-)
-```
-
-#### 2. Register in DI container
-
-Add the new domain to the dependency injection container (`internal/app/di.go`):
-
-```go
-// Add fields to Container struct
-type Container struct {
- // ... existing fields
- productRepo *productRepository.ProductRepository
- productUseCase productUsecase.UseCase // Interface, not concrete type
- productRepoInit sync.Once
- productUseCaseInit sync.Once
-}
-
-// Add getter methods
-func (c *Container) ProductRepository() (*productRepository.ProductRepository, error) {
- var err error
- c.productRepoInit.Do(func() {
- c.productRepo, err = c.initProductRepository()
- if err != nil {
- c.initErrors["productRepo"] = err
- }
- })
- // ... error handling
- 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() (productUsecase.ProductRepository, error) {
- db, err := c.DB()
- if err != nil {
- return nil, fmt.Errorf("failed to get database: %w", err)
- }
-
- // Select the appropriate repository based on the database driver
- switch c.config.DBDriver {
- case "mysql":
- return productRepository.NewMySQLProductRepository(db), nil
- case "postgres":
- return productRepository.NewPostgreSQLProductRepository(db), nil
- default:
- return nil, fmt.Errorf("unsupported database driver: %s", c.config.DBDriver)
- }
-}
-
-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
-
-Update `internal/http/server.go` to register product routes:
-
-```go
-productHandler := productHttp.NewProductHandler(container.ProductUseCase(), logger)
-mux.HandleFunc("/api/products", productHandler.HandleProducts)
-```
-
-**Tips:**
-- Define a UseCase interface in your usecase package to enable dependency inversion
-- Define domain-specific errors in your domain package by wrapping standard errors
-- Repository layer should transform infrastructure errors (like `sql.ErrNoRows`) to domain errors
-- Use case layer should return domain errors directly without additional wrapping
-- Use `httputil.HandleError()` in HTTP handlers for automatic error-to-HTTP status mapping
-- Use the shared `httputil.MakeJSONResponse` function 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, domain errors, and rules (e.g., `internal/user/domain`)
-2. **Repository Layer** - Data access implementations using `database/sql`; transforms infrastructure errors to domain errors (e.g., `internal/user/repository`)
-3. **Use Case Layer** - UseCase interfaces and application business logic; returns domain errors (e.g., `internal/user/usecase`)
-4. **Presentation Layer** - HTTP handlers that map domain errors to HTTP responses (e.g., `internal/user/http`)
-5. **Utility Layer** - Shared utilities including error handling and mapping (e.g., `internal/httputil`, `internal/errors`)
-
-**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.
-
-**Error Flow:** Errors flow from repository → use case → handler, where they are transformed from infrastructure concerns to domain concepts, and finally to appropriate HTTP responses.
-
-### Data Transfer Objects (DTOs)
-
-The project enforces clear boundaries between internal domain models and external API contracts using DTOs:
-
-**Domain Models** (`internal/user/domain/user.go`)
-- Pure internal representation of business entities
-- No JSON tags - completely decoupled from API serialization
-- Focus on business rules and domain logic
-
-**DTOs** (`internal/user/http/dto/`)
-- `request.go` - API request structures with validation
-- `response.go` - API response structures
-- `mapper.go` - Conversion functions between DTOs and domain models
-
-**Example:**
-
-```go
-// Request DTO
-type RegisterUserRequest struct {
- Name string `json:"name"`
- Email string `json:"email"`
- Password string `json:"password"`
-}
-
-func (r *RegisterUserRequest) Validate() error {
- if r.Name == "" {
- return errors.New("name is required")
- }
- // ... validation logic
-}
-
-// Response DTO
-type UserResponse struct {
- ID uuid.UUID `json:"id"`
- Name string `json:"name"`
- Email string `json:"email"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-// Mapper functions
-func ToRegisterUserInput(req RegisterUserRequest) usecase.RegisterUserInput {
- return usecase.RegisterUserInput{
- Name: req.Name,
- Email: req.Email,
- Password: req.Password,
- }
-}
-
-func ToUserResponse(user *domain.User) UserResponse {
- return UserResponse{
- ID: user.ID,
- Name: user.Name,
- Email: user.Email,
- CreatedAt: user.CreatedAt,
- UpdatedAt: user.UpdatedAt,
- }
-}
-```
-
-**Benefits:**
-1. **Separation of Concerns** - Domain models evolve independently from API contracts
-2. **Security** - Sensitive fields (like passwords) are never exposed in API responses
-3. **Flexibility** - Different API views of the same domain model (e.g., summary vs detailed)
-4. **Versioning** - Easy to maintain multiple API versions with different DTOs
-5. **Validation** - Request validation happens at the DTO level before reaching domain logic
-
-### Input Validation
-
-The project uses the [jellydator/validation](https://github.com/jellydator/validation) library for comprehensive input validation at both the DTO and use case layers.
-
-**Custom Validation Rules** (`internal/validation/rules.go`)
-
-The project provides reusable validation rules:
-
-```go
-// Password strength validation
-PasswordStrength{
- MinLength: 8,
- RequireUpper: true,
- RequireLower: true,
- RequireNumber: true,
- RequireSpecial: true,
-}
-
-// Email format validation
-Email
-
-// No leading/trailing whitespace
-NoWhitespace
-
-// Not blank after trimming
-NotBlank
-```
-
-**DTO Validation Example:**
-
-```go
-func (r *RegisterUserRequest) Validate() error {
- err := validation.ValidateStruct(r,
- validation.Field(&r.Name,
- validation.Required.Error("name is required"),
- appValidation.NotBlank,
- validation.Length(1, 255).Error("name must be between 1 and 255 characters"),
- ),
- validation.Field(&r.Email,
- validation.Required.Error("email is required"),
- appValidation.NotBlank,
- appValidation.Email,
- validation.Length(5, 255).Error("email must be between 5 and 255 characters"),
- ),
- validation.Field(&r.Password,
- validation.Required.Error("password is required"),
- validation.Length(8, 128).Error("password must be between 8 and 128 characters"),
- appValidation.PasswordStrength{
- MinLength: 8,
- RequireUpper: true,
- RequireLower: true,
- RequireNumber: true,
- RequireSpecial: true,
- },
- ),
- )
- return appValidation.WrapValidationError(err)
-}
-```
-
-**Validation Layers:**
-
-1. **DTO Layer** - Validates API request structure and basic constraints
-2. **Use Case Layer** - Validates business logic rules and constraints
-3. **Domain Layer** - Defines domain-specific error types
-
-**Error Responses:**
-
-Validation errors are automatically wrapped as `ErrInvalidInput` and return 422 Unprocessable Entity:
-
-```json
-{
- "error": "invalid_input",
- "message": "password: password must contain at least one uppercase letter."
-}
-```
-
-**Benefits:**
-- **Declarative** - Validation rules are clear and concise
-- **Reusable** - Custom rules can be shared across the application
-- **Type-Safe** - Compile-time validation of struct fields
-- **Extensible** - Easy to add custom validation rules
-- **Consistent** - Same validation logic at DTO and use case layers
-- **User-Friendly** - Detailed error messages help API clients fix issues
-
-### Transaction Management
-
-The template implements a TxManager interface for handling database transactions:
-
-```go
-type TxManager interface {
- WithTx(ctx context.Context, fn func(ctx context.Context) error) error
-}
-```
-
-Transactions are automatically injected into the context and used by repositories.
-
-### HTTP Utilities
-
-The `httputil` package provides shared HTTP utilities used across all domain modules:
-
-**MakeJSONResponse** - Standardized JSON response formatting:
-
-```go
-import "github.com/allisson/go-project-template/internal/httputil"
-
-func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
- product, err := h.productUseCase.GetProduct(r.Context(), productID)
- if err != nil {
- httputil.HandleError(w, err, h.logger)
- return
- }
-
- httputil.MakeJSONResponse(w, http.StatusOK, product)
-}
-```
-
-**HandleError** - Automatic domain error to HTTP status code mapping:
-
-```go
-func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
- user, err := h.userUseCase.RegisterUser(r.Context(), input)
- if err != nil {
- // Automatically maps domain errors to appropriate HTTP status codes
- // ErrNotFound → 404, ErrConflict → 409, ErrInvalidInput → 422, etc.
- httputil.HandleError(w, err, h.logger)
- return
- }
- httputil.MakeJSONResponse(w, http.StatusCreated, response)
-}
-```
-
-**HandleValidationError** - For JSON decode and validation errors:
-
-```go
-if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- httputil.HandleValidationError(w, err, h.logger) // Returns 400 Bad Request
- return
-}
-```
-
-These utilities ensure consistent response formatting and error handling across all HTTP endpoints.
-
-### Transactional Outbox Pattern
-
-User registration demonstrates the transactional outbox pattern:
-
-1. User is created in the database
-2. `user.created` event is stored in the outbox table (same transaction)
-3. Worker picks up pending events and processes them
-4. Events are marked as processed or failed
-
-This guarantees that events are never lost and provides at-least-once delivery.
-
-## Configuration
-
-All configuration is done via environment variables. The application automatically loads a `.env` file if present (searching recursively from the current directory up to the root).
-
-### Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `SERVER_HOST` | HTTP server host | `0.0.0.0` |
-| `SERVER_PORT` | HTTP server port | `8080` |
-| `DB_DRIVER` | Database driver (postgres/mysql) | `postgres` |
-| `DB_CONNECTION_STRING` | Database connection string | `postgres://user:password@localhost:5432/mydb?sslmode=disable` |
-| `DB_MAX_OPEN_CONNECTIONS` | Max open connections | `25` |
-| `DB_MAX_IDLE_CONNECTIONS` | Max idle connections | `5` |
-| `DB_CONN_MAX_LIFETIME` | Connection max lifetime | `5` |
-| `LOG_LEVEL` | Log level (debug/info/warn/error) | `info` |
-| `WORKER_INTERVAL` | Worker poll interval | `5` |
-| `WORKER_BATCH_SIZE` | Events to process per batch | `10` |
-| `WORKER_MAX_RETRIES` | Max retry attempts | `3` |
-| `WORKER_RETRY_INTERVAL` | Retry interval | `1` |
-
-## Database Migrations
-
-Migrations are located in `migrations/postgresql` and `migrations/mysql` directories.
-
-### Creating new migrations
-
-1. Create new `.up.sql` and `.down.sql` files with sequential numbering
-2. Follow the naming convention: `000003_description.up.sql`
-
-### Running migrations manually
-
-Use the golang-migrate CLI:
-
-```bash
-migrate -path migrations/postgresql -database "postgres://user:password@localhost:5432/mydb?sslmode=disable" up
-```
-
-## Testing
-
-The project uses real databases (PostgreSQL and MySQL) for testing instead of mocks, ensuring tests accurately reflect production behavior.
-
-### Test Infrastructure
-
-Tests use Docker Compose to run test databases with dedicated test credentials and ports:
-
-- **PostgreSQL**: `localhost:5433` (testuser/testpassword/testdb)
-- **MySQL**: `localhost:3307` (testuser/testpassword/testdb)
-
-The test helper utilities (`internal/testutil/database.go`) automatically:
-1. Connect to test databases
-2. Run migrations automatically before tests
-3. Clean up data between tests to prevent pollution
-4. Provide isolated test environments
-
-**Important:** Both local development (via Docker Compose) and CI (via GitHub Actions) use identical database configurations, ensuring tests behave the same in all environments.
-
-### Running Tests
-
-**Start test databases:**
-```bash
-make test-db-up
-```
-
-**Run all tests:**
```bash
-make test
-```
-
-**Run tests with coverage:**
-```bash
-make test-coverage
-```
-
-**Run tests and manage databases automatically:**
-```bash
-make test-with-db # Starts databases, runs tests, stops databases
-```
-
-**Stop test databases:**
-```bash
-make test-db-down
-```
-
-### Running Tests Locally
-
-```bash
-# With test databases already running
-go test -v -race ./...
-```
-
-### Test Structure
-
-Tests use real database connections instead of mocks:
-
-```go
-func TestPostgreSQLUserRepository_Create(t *testing.T) {
- db := testutil.SetupPostgresDB(t) // Connect and run migrations
- defer testutil.TeardownDB(t, db) // Clean up connection
- defer testutil.CleanupPostgresDB(t, db) // Clean up test data
-
- repo := NewPostgreSQLUserRepository(db)
- // ... test implementation
-}
+DB_DRIVER=postgres
+DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
+SERVER_HOST=0.0.0.0
+SERVER_PORT=8080
+LOG_LEVEL=info
```
-**Benefits of using real databases:**
-- Tests verify actual SQL queries and database interactions
-- Catches database-specific issues (constraints, types, etc.)
-- Tests reflect production behavior more accurately
-- No need to maintain mock expectations
+See the [Getting Started Guide](docs/getting-started.md) for all available configuration options.
-### CI/CD Testing
+## ➕ Adding New Domains
-The GitHub Actions workflow automatically:
-1. Starts PostgreSQL (port 5433) and MySQL (port 3307) containers
-2. Waits for both databases to be healthy
-3. Runs all tests with race detection against both databases
-4. Generates and uploads coverage reports to Codecov
+Adding a new domain is straightforward:
-**CI Configuration:**
-- Uses the same database credentials as local tests (testuser/testpassword/testdb)
-- Same port mappings as Docker Compose (5433 for Postgres, 3307 for MySQL)
-- Runs on every push to `main` and all pull requests
-- All tests must pass before merging
+1. Create domain structure (`domain/`, `usecase/`, `repository/`, `http/`)
+2. Define domain entity and errors
+3. Create database migrations
+4. Implement repositories (PostgreSQL and MySQL)
+5. Implement use case with business logic
+6. Create DTOs and HTTP handlers
+7. Register in DI container
+8. Wire HTTP routes
-This ensures complete consistency between local development and CI environments.
+See the [Adding Domains Guide](docs/adding-domains.md) for a complete step-by-step tutorial.
-## Dependencies
+## 📦 Dependencies
### Core Libraries
-- [go-env](https://github.com/allisson/go-env) - Environment variable configuration
-- [godotenv](https://github.com/joho/godotenv) - Loads environment variables from .env files
-- [go-pwdhash](https://github.com/allisson/go-pwdhash) - Password hashing with Argon2id
-- [validation](https://github.com/jellydator/validation) - Advanced input validation library
-- [uuid](https://github.com/google/uuid) - UUID generation including UUIDv7 support
+- [google/uuid](https://github.com/google/uuid) - UUID generation (UUIDv7 support)
+- [jellydator/validation](https://github.com/jellydator/validation) - Advanced input validation
- [urfave/cli](https://github.com/urfave/cli) - CLI framework
-- [golang-migrate](https://github.com/golang-migrate/migrate) - Database migrations
+- [allisson/go-env](https://github.com/allisson/go-env) - Environment configuration
+- [allisson/go-pwdhash](https://github.com/allisson/go-pwdhash) - Argon2id password hashing
+- [golang-migrate/migrate](https://github.com/golang-migrate/migrate) - Database migrations
### Database Drivers
- [lib/pq](https://github.com/lib/pq) - PostgreSQL driver
- [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) - MySQL driver
-## License
+## 🤝 Contributing
-MIT License - see LICENSE file for details
+Contributions are welcome! Please feel free to submit a Pull Request.
-## Contributing
+1. Fork the repository
+2. Create your feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
-Contributions are welcome! Please feel free to submit a Pull Request.
+## 📄 License
-## Acknowledgments
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
-This template uses the following excellent Go libraries:
+## 🙏 Acknowledgments
+
+This template leverages these excellent Go libraries:
- github.com/allisson/go-env
- github.com/allisson/go-pwdhash
- github.com/jellydator/validation
+- github.com/google/uuid
- github.com/urfave/cli
-- github.com/golang-migrate/migrate
\ No newline at end of file
+- github.com/golang-migrate/migrate
+
+---
+
+
diff --git a/docs/adding-domains.md b/docs/adding-domains.md
new file mode 100644
index 0000000..8d21003
--- /dev/null
+++ b/docs/adding-domains.md
@@ -0,0 +1,724 @@
+# ➕ Adding New Domains
+
+This guide walks you through adding a new domain to the project. We'll use a `product` domain as an example.
+
+## 🏗️ Domain Structure
+
+Each domain follows this structure:
+
+```
+internal/product/
+├── domain/
+│ └── product.go # Entity + domain errors
+├── usecase/
+│ └── product_usecase.go # UseCase interface + implementation
+├── repository/
+│ ├── mysql_product_repository.go
+│ └── postgresql_product_repository.go
+└── http/
+ ├── dto/
+ │ ├── request.go # Request DTOs with validation
+ │ ├── response.go # Response DTOs with JSON tags
+ │ └── mapper.go # DTO ↔ domain conversions
+ └── product_handler.go # HTTP handlers
+```
+
+## 📝 Step-by-Step Guide
+
+### Step 1: Create Domain Entity
+
+Create `internal/product/domain/product.go`:
+
+```go
+package domain
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+
+ apperrors "github.com/yourproject/internal/errors"
+)
+
+// Product represents a product in the system
+type Product struct {
+ ID uuid.UUID
+ Name string
+ Description string
+ Price float64
+ Stock int
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+// Domain-specific errors
+var (
+ ErrProductNotFound = apperrors.Wrap(apperrors.ErrNotFound, "product not found")
+ ErrInsufficientStock = apperrors.Wrap(apperrors.ErrConflict, "insufficient stock")
+ ErrInvalidPrice = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid price")
+ ErrInvalidQuantity = apperrors.Wrap(apperrors.ErrInvalidInput, "quantity must be positive")
+)
+```
+
+**Key Points**:
+- ✅ No JSON tags (domain models are pure)
+- ✅ Use `uuid.UUID` for ID
+- ✅ Wrap standard errors with domain-specific messages
+
+### Step 2: Create Database Migrations
+
+Create migration files for both databases:
+
+**PostgreSQL** (`migrations/postgresql/000003_create_products_table.up.sql`):
+
+```sql
+CREATE TABLE products (
+ id UUID PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ price DECIMAL(10, 2) NOT NULL,
+ stock INTEGER NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX idx_products_name ON products(name);
+```
+
+**PostgreSQL Down** (`migrations/postgresql/000003_create_products_table.down.sql`):
+
+```sql
+DROP INDEX IF EXISTS idx_products_name;
+DROP TABLE IF EXISTS products;
+```
+
+**MySQL** (`migrations/mysql/000003_create_products_table.up.sql`):
+
+```sql
+CREATE TABLE products (
+ id BINARY(16) PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ price DECIMAL(10, 2) NOT NULL,
+ stock INTEGER NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_products_name (name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+**MySQL Down** (`migrations/mysql/000003_create_products_table.down.sql`):
+
+```sql
+DROP TABLE IF EXISTS products;
+```
+
+Run migrations:
+```bash
+make run-migrate
+```
+
+### Step 3: Create Repository Interface & Implementations
+
+**Define Repository Interface** in `internal/product/usecase/product_usecase.go`:
+
+```go
+package usecase
+
+import (
+ "context"
+
+ "github.com/google/uuid"
+
+ "github.com/yourproject/internal/product/domain"
+)
+
+// ProductRepository defines the interface for product data access
+type ProductRepository interface {
+ Create(ctx context.Context, product *domain.Product) error
+ GetByID(ctx context.Context, id uuid.UUID) (*domain.Product, error)
+ Update(ctx context.Context, product *domain.Product) error
+ Delete(ctx context.Context, id uuid.UUID) error
+ List(ctx context.Context, limit, offset int) ([]*domain.Product, error)
+}
+```
+
+**PostgreSQL Implementation** (`internal/product/repository/postgresql_product_repository.go`):
+
+```go
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "strings"
+
+ "github.com/google/uuid"
+
+ apperrors "github.com/yourproject/internal/errors"
+ "github.com/yourproject/internal/database"
+ "github.com/yourproject/internal/product/domain"
+)
+
+type PostgreSQLProductRepository struct {
+ db *sql.DB
+}
+
+func NewPostgreSQLProductRepository(db *sql.DB) *PostgreSQLProductRepository {
+ return &PostgreSQLProductRepository{db: db}
+}
+
+func (r *PostgreSQLProductRepository) Create(ctx context.Context, product *domain.Product) error {
+ querier := database.GetTx(ctx, r.db)
+
+ query := `INSERT INTO products (id, name, description, price, stock, created_at, updated_at)
+ VALUES ($1, $2, $3, $4, $5, NOW(), NOW())`
+
+ _, err := querier.ExecContext(ctx, query,
+ product.ID,
+ product.Name,
+ product.Description,
+ product.Price,
+ product.Stock,
+ )
+ if err != nil {
+ if isPostgreSQLUniqueViolation(err) {
+ return apperrors.Wrap(apperrors.ErrConflict, "product with this name already exists")
+ }
+ return apperrors.Wrap(err, "failed to create product")
+ }
+ return nil
+}
+
+func (r *PostgreSQLProductRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Product, error) {
+ querier := database.GetTx(ctx, r.db)
+
+ query := `SELECT id, name, description, price, stock, created_at, updated_at
+ FROM products WHERE id = $1`
+
+ var product domain.Product
+ err := querier.QueryRowContext(ctx, query, id).Scan(
+ &product.ID,
+ &product.Name,
+ &product.Description,
+ &product.Price,
+ &product.Stock,
+ &product.CreatedAt,
+ &product.UpdatedAt,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, domain.ErrProductNotFound
+ }
+ return nil, apperrors.Wrap(err, "failed to get product by id")
+ }
+ return &product, nil
+}
+
+func isPostgreSQLUniqueViolation(err error) bool {
+ return strings.Contains(err.Error(), "duplicate key") ||
+ strings.Contains(err.Error(), "unique constraint")
+}
+```
+
+**MySQL Implementation** (`internal/product/repository/mysql_product_repository.go`):
+
+```go
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "strings"
+
+ "github.com/google/uuid"
+
+ apperrors "github.com/yourproject/internal/errors"
+ "github.com/yourproject/internal/database"
+ "github.com/yourproject/internal/product/domain"
+)
+
+type MySQLProductRepository struct {
+ db *sql.DB
+}
+
+func NewMySQLProductRepository(db *sql.DB) *MySQLProductRepository {
+ return &MySQLProductRepository{db: db}
+}
+
+func (r *MySQLProductRepository) Create(ctx context.Context, product *domain.Product) error {
+ querier := database.GetTx(ctx, r.db)
+
+ query := `INSERT INTO products (id, name, description, price, stock, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, NOW(), NOW())`
+
+ // Convert UUID to bytes for MySQL BINARY(16)
+ uuidBytes, err := product.ID.MarshalBinary()
+ if err != nil {
+ return apperrors.Wrap(err, "failed to marshal UUID")
+ }
+
+ _, err = querier.ExecContext(ctx, query,
+ uuidBytes,
+ product.Name,
+ product.Description,
+ product.Price,
+ product.Stock,
+ )
+ if err != nil {
+ if isMySQLUniqueViolation(err) {
+ return apperrors.Wrap(apperrors.ErrConflict, "product with this name already exists")
+ }
+ return apperrors.Wrap(err, "failed to create product")
+ }
+ return nil
+}
+
+func isMySQLUniqueViolation(err error) bool {
+ return strings.Contains(err.Error(), "Error 1062") ||
+ strings.Contains(err.Error(), "Duplicate entry")
+}
+```
+
+### Step 4: Create Use Case
+
+Create `internal/product/usecase/product_usecase.go`:
+
+```go
+package usecase
+
+import (
+ "context"
+
+ "github.com/google/uuid"
+ validation "github.com/jellydator/validation"
+
+ apperrors "github.com/yourproject/internal/errors"
+ "github.com/yourproject/internal/database"
+ "github.com/yourproject/internal/product/domain"
+)
+
+// UseCase defines the interface for product business logic
+type UseCase interface {
+ CreateProduct(ctx context.Context, input CreateProductInput) (*domain.Product, error)
+ GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error)
+ UpdateProduct(ctx context.Context, id uuid.UUID, input UpdateProductInput) (*domain.Product, error)
+ DeleteProduct(ctx context.Context, id uuid.UUID) error
+ ListProducts(ctx context.Context, limit, offset int) ([]*domain.Product, error)
+}
+
+type ProductUseCase struct {
+ txManager database.TxManager
+ productRepo ProductRepository
+}
+
+func NewProductUseCase(txManager database.TxManager, productRepo ProductRepository) *ProductUseCase {
+ return &ProductUseCase{
+ txManager: txManager,
+ productRepo: productRepo,
+ }
+}
+
+// CreateProductInput represents the input for creating a product
+type CreateProductInput struct {
+ Name string
+ Description string
+ Price float64
+ Stock int
+}
+
+func (i CreateProductInput) Validate() error {
+ return validation.ValidateStruct(&i,
+ validation.Field(&i.Name, validation.Required, validation.Length(1, 255)),
+ validation.Field(&i.Price, validation.Required, validation.Min(0.0)),
+ validation.Field(&i.Stock, validation.Required, validation.Min(0)),
+ )
+}
+
+func (uc *ProductUseCase) CreateProduct(ctx context.Context, input CreateProductInput) (*domain.Product, error) {
+ if err := input.Validate(); err != nil {
+ return nil, apperrors.Wrap(apperrors.ErrInvalidInput, err.Error())
+ }
+
+ if input.Price <= 0 {
+ return nil, domain.ErrInvalidPrice
+ }
+
+ if input.Stock < 0 {
+ return nil, domain.ErrInvalidQuantity
+ }
+
+ product := &domain.Product{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: input.Name,
+ Description: input.Description,
+ Price: input.Price,
+ Stock: input.Stock,
+ }
+
+ if err := uc.productRepo.Create(ctx, product); err != nil {
+ return nil, err
+ }
+
+ return product, nil
+}
+
+func (uc *ProductUseCase) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) {
+ return uc.productRepo.GetByID(ctx, id)
+}
+```
+
+### Step 5: Create DTOs
+
+**Request DTO** (`internal/product/http/dto/request.go`):
+
+```go
+package dto
+
+import (
+ validation "github.com/jellydator/validation"
+
+ appValidation "github.com/yourproject/internal/validation"
+)
+
+type CreateProductRequest struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Price float64 `json:"price"`
+ Stock int `json:"stock"`
+}
+
+func (r *CreateProductRequest) Validate() error {
+ err := validation.ValidateStruct(r,
+ validation.Field(&r.Name,
+ validation.Required.Error("name is required"),
+ appValidation.NotBlank,
+ validation.Length(1, 255),
+ ),
+ validation.Field(&r.Price,
+ validation.Required.Error("price is required"),
+ validation.Min(0.01).Error("price must be greater than 0"),
+ ),
+ validation.Field(&r.Stock,
+ validation.Required.Error("stock is required"),
+ validation.Min(0).Error("stock cannot be negative"),
+ ),
+ )
+ return appValidation.WrapValidationError(err)
+}
+```
+
+**Response DTO** (`internal/product/http/dto/response.go`):
+
+```go
+package dto
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type ProductResponse struct {
+ ID uuid.UUID `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Price float64 `json:"price"`
+ Stock int `json:"stock"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+```
+
+**Mapper** (`internal/product/http/dto/mapper.go`):
+
+```go
+package dto
+
+import (
+ "github.com/yourproject/internal/product/domain"
+ "github.com/yourproject/internal/product/usecase"
+)
+
+func ToCreateProductInput(req CreateProductRequest) usecase.CreateProductInput {
+ return usecase.CreateProductInput{
+ Name: req.Name,
+ Description: req.Description,
+ Price: req.Price,
+ Stock: req.Stock,
+ }
+}
+
+func ToProductResponse(product *domain.Product) ProductResponse {
+ return ProductResponse{
+ ID: product.ID,
+ Name: product.Name,
+ Description: product.Description,
+ Price: product.Price,
+ Stock: product.Stock,
+ CreatedAt: product.CreatedAt,
+ UpdatedAt: product.UpdatedAt,
+ }
+}
+
+func ToProductListResponse(products []*domain.Product) []ProductResponse {
+ responses := make([]ProductResponse, len(products))
+ for i, product := range products {
+ responses[i] = ToProductResponse(product)
+ }
+ return responses
+}
+```
+
+### Step 6: Create HTTP Handler
+
+Create `internal/product/http/product_handler.go`:
+
+```go
+package http
+
+import (
+ "encoding/json"
+ "log/slog"
+ "net/http"
+
+ "github.com/google/uuid"
+
+ "github.com/yourproject/internal/httputil"
+ "github.com/yourproject/internal/product/http/dto"
+ "github.com/yourproject/internal/product/usecase"
+)
+
+type ProductHandler struct {
+ productUseCase usecase.UseCase
+ logger *slog.Logger
+}
+
+func NewProductHandler(productUseCase usecase.UseCase, logger *slog.Logger) *ProductHandler {
+ return &ProductHandler{
+ productUseCase: productUseCase,
+ logger: logger,
+ }
+}
+
+func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
+ var req dto.CreateProductRequest
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ httputil.HandleValidationError(w, err, h.logger)
+ return
+ }
+
+ if err := req.Validate(); err != nil {
+ httputil.HandleError(w, err, h.logger)
+ return
+ }
+
+ input := dto.ToCreateProductInput(req)
+ product, err := h.productUseCase.CreateProduct(r.Context(), input)
+ if err != nil {
+ httputil.HandleError(w, err, h.logger)
+ return
+ }
+
+ response := dto.ToProductResponse(product)
+ httputil.MakeJSONResponse(w, http.StatusCreated, response)
+}
+
+func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
+ // Parse product ID from URL
+ idStr := r.PathValue("id")
+ id, err := uuid.Parse(idStr)
+ if err != nil {
+ httputil.HandleValidationError(w, err, h.logger)
+ return
+ }
+
+ product, err := h.productUseCase.GetProduct(r.Context(), id)
+ if err != nil {
+ httputil.HandleError(w, err, h.logger)
+ return
+ }
+
+ response := dto.ToProductResponse(product)
+ httputil.MakeJSONResponse(w, http.StatusOK, response)
+}
+```
+
+### Step 7: Register in DI Container
+
+Update `internal/app/di.go`:
+
+```go
+type Container struct {
+ // ... existing fields
+ productRepo productUsecase.ProductRepository
+ productUseCase productUsecase.UseCase // Interface, not concrete type!
+ productRepoInit sync.Once
+ productUseCaseInit sync.Once
+}
+
+func (c *Container) ProductRepository() (productUsecase.ProductRepository, error) {
+ var err error
+ c.productRepoInit.Do(func() {
+ c.productRepo, err = c.initProductRepository()
+ if err != nil {
+ c.initErrors["productRepo"] = err
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+ if existingErr, ok := c.initErrors["productRepo"]; ok {
+ return nil, existingErr
+ }
+ 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
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+ if existingErr, ok := c.initErrors["productUseCase"]; ok {
+ return nil, existingErr
+ }
+ return c.productUseCase, nil
+}
+
+func (c *Container) initProductRepository() (productUsecase.ProductRepository, error) {
+ db, err := c.DB()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get database: %w", err)
+ }
+
+ switch c.config.DBDriver {
+ case "mysql":
+ return productRepository.NewMySQLProductRepository(db), nil
+ case "postgres":
+ return productRepository.NewPostgreSQLProductRepository(db), nil
+ default:
+ return nil, fmt.Errorf("unsupported database driver: %s", c.config.DBDriver)
+ }
+}
+
+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), nil
+}
+```
+
+### Step 8: Wire HTTP Routes
+
+Update `internal/http/server.go`:
+
+```go
+func (s *Server) setupRoutes() {
+ mux := http.NewServeMux()
+
+ // Health checks
+ mux.HandleFunc("/health", s.handleHealth)
+ mux.HandleFunc("/ready", s.handleReady)
+
+ // User routes
+ userHandler := userHttp.NewUserHandler(s.container.UserUseCase(), s.logger)
+ mux.HandleFunc("POST /api/users", userHandler.RegisterUser)
+
+ // Product routes
+ productHandler := productHttp.NewProductHandler(s.container.ProductUseCase(), s.logger)
+ mux.HandleFunc("POST /api/products", productHandler.CreateProduct)
+ mux.HandleFunc("GET /api/products/{id}", productHandler.GetProduct)
+
+ s.handler = s.middleware(mux)
+}
+```
+
+### Step 9: Write Tests
+
+Create tests for your new domain following the [Testing Guide](testing.md).
+
+### Step 10: Test Your New Domain
+
+```bash
+# Run migrations
+make run-migrate
+
+# Start server
+make run-server
+
+# Create a product
+curl -X POST http://localhost:8080/api/products \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Laptop",
+ "description": "High-performance laptop",
+ "price": 999.99,
+ "stock": 10
+ }'
+
+# Get a product
+curl http://localhost:8080/api/products/{id}
+```
+
+## ✅ Checklist
+
+Use this checklist when adding a new domain:
+
+- [ ] Create domain entity with business logic
+- [ ] Define domain-specific errors
+- [ ] Create database migrations (PostgreSQL and MySQL)
+- [ ] Run migrations
+- [ ] Define repository interface in use case package
+- [ ] Implement PostgreSQL repository
+- [ ] Implement MySQL repository
+- [ ] Create use case interface
+- [ ] Implement use case with business logic
+- [ ] Create request DTOs with validation
+- [ ] Create response DTOs with JSON tags
+- [ ] Create mapper functions
+- [ ] Create HTTP handlers
+- [ ] Register repositories in DI container
+- [ ] Register use cases in DI container
+- [ ] Wire HTTP routes in server
+- [ ] Write repository tests (PostgreSQL and MySQL)
+- [ ] Write use case tests
+- [ ] Write HTTP handler tests
+- [ ] Run linter and fix issues
+- [ ] Test endpoints manually
+- [ ] Update documentation if needed
+
+## 🎯 Best Practices
+
+- ✅ **Follow existing patterns** - Look at the `user` domain for reference
+- ✅ **Keep domain models pure** - No JSON tags, no framework dependencies
+- ✅ **Define clear interfaces** - Use case and repository interfaces
+- ✅ **Wrap standard errors** - Create domain-specific errors
+- ✅ **Validate input** - At both DTO and use case levels
+- ✅ **Use transactions** - When multiple operations must be atomic
+- ✅ **Write tests** - Integration tests with real databases
+- ✅ **Document code** - Add comments for complex logic
+- ✅ **Run linter** - Before committing changes
+
+## 📚 Related Documentation
+
+- [Architecture](architecture.md) - Understand the architectural patterns
+- [Error Handling](error-handling.md) - Learn the error handling system
+- [Development](development.md) - Development workflow and guidelines
+- [Testing](testing.md) - Writing effective tests
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..499c623
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,406 @@
+# 🏗️ Architecture
+
+This document describes the architectural patterns and design principles used in this Go project template.
+
+## 📐 Clean Architecture Layers
+
+The project follows Clean Architecture principles with clear separation of concerns across five layers:
+
+### 1. Domain Layer 🎯
+**Location**: `internal/{domain}/domain/`
+
+Contains business entities, domain errors, and business rules.
+
+**Responsibilities**:
+- Define entities with pure business logic (no JSON tags)
+- Define domain-specific errors by wrapping standard errors
+- Implement domain validation rules
+
+**Example**:
+```go
+// internal/user/domain/user.go
+package domain
+
+import (
+ "time"
+ "github.com/google/uuid"
+ "github.com/allisson/go-project-template/internal/errors"
+)
+
+type User struct {
+ ID uuid.UUID
+ Name string
+ Email string
+ Password string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+// Domain-specific errors
+var (
+ ErrUserNotFound = errors.Wrap(errors.ErrNotFound, "user not found")
+ ErrUserAlreadyExists = errors.Wrap(errors.ErrConflict, "user already exists")
+ ErrInvalidEmail = errors.Wrap(errors.ErrInvalidInput, "invalid email format")
+)
+```
+
+### 2. Repository Layer 💾
+**Location**: `internal/{domain}/repository/`
+
+Handles data persistence and retrieval. Implements separate repositories for MySQL and PostgreSQL.
+
+**Responsibilities**:
+- Implement data access for each database type
+- Transform infrastructure errors to domain errors (e.g., `sql.ErrNoRows` → `domain.ErrUserNotFound`)
+- Use `database.GetTx(ctx, r.db)` to support transactions
+- Handle database-specific concerns (UUID marshaling, placeholder syntax, etc.)
+
+**Key Differences**:
+| Feature | MySQL | PostgreSQL |
+|---------|-------|------------|
+| UUID Storage | `BINARY(16)` - requires marshaling/unmarshaling | Native `UUID` type |
+| Placeholders | `?` for all parameters | `$1, $2, $3...` numbered parameters |
+| Unique Errors | Check for "1062" or "duplicate entry" | Check for "duplicate key" or "unique constraint" |
+
+**Example**:
+```go
+// internal/user/repository/postgresql_user_repository.go
+func (r *PostgreSQLUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
+ querier := database.GetTx(ctx, r.db)
+ query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1`
+
+ var user domain.User
+ err := querier.QueryRowContext(ctx, query, id).Scan(
+ &user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, domain.ErrUserNotFound // Transform to domain error
+ }
+ return nil, apperrors.Wrap(err, "failed to get user by id")
+ }
+ return &user, nil
+}
+```
+
+### 3. Use Case Layer 💼
+**Location**: `internal/{domain}/usecase/`
+
+Implements business logic and orchestrates domain operations.
+
+**Responsibilities**:
+- Define UseCase interfaces for dependency inversion
+- Implement business logic and orchestration
+- Validate input using `github.com/jellydator/validation`
+- Return domain errors directly without additional wrapping
+- Manage transactions using `TxManager.WithTx()`
+
+**Example**:
+```go
+// internal/user/usecase/user_usecase.go
+type UseCase interface {
+ RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error)
+}
+
+func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
+ // Validate input
+ if err := input.Validate(); err != nil {
+ return nil, err
+ }
+
+ // Business logic
+ hashedPassword, err := uc.passwordHasher.Hash([]byte(input.Password))
+ if err != nil {
+ return nil, apperrors.Wrap(err, "failed to hash password")
+ }
+
+ user := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: input.Name,
+ Email: input.Email,
+ Password: string(hashedPassword),
+ }
+
+ // Transaction management
+ err = uc.txManager.WithTx(ctx, func(ctx context.Context) error {
+ if err := uc.userRepo.Create(ctx, user); err != nil {
+ return err // Pass through domain errors
+ }
+ // Create outbox event in same transaction
+ event := &outboxDomain.OutboxEvent{
+ ID: uuid.Must(uuid.NewV7()),
+ EventType: "user.created",
+ Payload: string(payload),
+ Status: outboxDomain.StatusPending,
+ }
+ return uc.outboxRepo.Create(ctx, event)
+ })
+
+ return user, err
+}
+```
+
+### 4. Presentation Layer 🌐
+**Location**: `internal/{domain}/http/`
+
+Contains HTTP handlers and DTOs (Data Transfer Objects).
+
+**Responsibilities**:
+- Define request/response DTOs with JSON tags
+- Validate DTOs using `jellydator/validation`
+- Use `httputil.HandleError()` for automatic error-to-HTTP status mapping
+- Use `httputil.MakeJSONResponse()` for consistent JSON responses
+- Depend on UseCase interfaces, not concrete implementations
+
+**DTO Structure**:
+- `dto/request.go` - Request DTOs with validation
+- `dto/response.go` - Response DTOs with JSON tags
+- `dto/mapper.go` - Conversion functions between DTOs and domain models
+
+**Example**:
+```go
+// internal/user/http/user_handler.go
+func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
+ var req dto.RegisterUserRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ httputil.HandleValidationError(w, err, h.logger)
+ return
+ }
+
+ if err := req.Validate(); err != nil {
+ httputil.HandleError(w, err, h.logger)
+ return
+ }
+
+ input := dto.ToRegisterUserInput(req)
+ user, err := h.userUseCase.RegisterUser(r.Context(), input)
+ if err != nil {
+ httputil.HandleError(w, err, h.logger) // Auto-maps domain errors to HTTP status
+ return
+ }
+
+ response := dto.ToUserResponse(user)
+ httputil.MakeJSONResponse(w, http.StatusCreated, response)
+}
+```
+
+### 5. Utility Layer 🛠️
+**Location**: `internal/{httputil,errors,validation}/`
+
+Provides shared utilities for error handling, HTTP responses, and validation.
+
+**Components**:
+- **`internal/errors/`** - Standardized domain errors (ErrNotFound, ErrConflict, etc.)
+- **`internal/httputil/`** - HTTP utilities (JSON responses, error mapping)
+- **`internal/validation/`** - Custom validation rules (email, password strength, etc.)
+
+## 🔄 Dependency Inversion Principle
+
+The project follows the Dependency Inversion Principle where:
+- **High-level modules** (use cases) define interfaces
+- **Low-level modules** (repositories, handlers) implement those interfaces
+- **Dependencies point inward** towards the domain
+
+```
+┌─────────────────────────────────────────┐
+│ Presentation Layer (HTTP) │
+│ - Depends on UseCase interfaces │
+└──────────────────┬──────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────┐
+│ Use Case Layer │
+│ - Defines interfaces │
+│ - Implements business logic │
+└──────────────────┬──────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────┐
+│ Domain Layer │
+│ - Pure business entities │
+│ - No external dependencies │
+└──────────────────┬──────────────────────┘
+ │
+ ▲
+┌──────────────────┴──────────────────────┐
+│ Repository Layer │
+│ - Implements repository interfaces │
+│ - Depends on domain entities │
+└─────────────────────────────────────────┘
+```
+
+## 📦 Modular Domain Architecture
+
+Each business domain is organized in its own directory with clear separation of concerns:
+
+```
+internal/
+├── user/ # User domain module
+│ ├── domain/ # User entities and domain errors
+│ │ └── user.go
+│ ├── usecase/ # User business logic
+│ │ └── user_usecase.go
+│ ├── repository/ # User data access
+│ │ ├── mysql_user_repository.go
+│ │ └── postgresql_user_repository.go
+│ └── http/ # User HTTP handlers
+│ ├── dto/ # Request/response DTOs
+│ │ ├── request.go
+│ │ ├── response.go
+│ │ └── mapper.go
+│ └── user_handler.go
+├── outbox/ # Outbox domain module
+│ ├── domain/
+│ └── repository/
+└── {new-domain}/ # Easy to add new domains
+```
+
+**Benefits**:
+1. 🎯 **Scalability** - Easy to add new domains without affecting existing code
+2. 🔒 **Encapsulation** - Each domain is self-contained with clear boundaries
+3. 👥 **Team Collaboration** - Teams can work on different domains independently
+4. 🔧 **Maintainability** - Related code is co-located
+
+## 🔌 Dependency Injection Container
+
+The DI container (`internal/app/`) manages all application components with:
+
+- **Centralized component wiring** - All dependencies assembled in one place
+- **Lazy initialization** - Components created only when first accessed
+- **Singleton pattern** - Each component initialized once and reused
+- **Clean resource management** - Unified shutdown for all resources
+- **Thread-safe** - Safe for concurrent access across goroutines
+
+**Dependency Graph**:
+```
+Container
+├── Infrastructure (Database, Logger)
+├── Repositories (User, Outbox)
+├── Use Cases (User)
+└── Presentation (HTTP Server, Worker)
+```
+
+**Example**:
+```go
+// Create container with configuration
+container := app.NewContainer(cfg)
+
+// Get HTTP server (automatically initializes all dependencies)
+server, err := container.HTTPServer()
+if err != nil {
+ return fmt.Errorf("failed to initialize HTTP server: %w", err)
+}
+
+// Clean shutdown
+defer container.Shutdown(ctx)
+```
+
+For more details, see [`internal/app/README.md`](../internal/app/README.md).
+
+## 🆔 UUIDv7 Primary Keys
+
+The project uses **UUIDv7** for all primary keys instead of auto-incrementing integers.
+
+**Benefits**:
+- ⏱️ **Time-ordered**: UUIDs include timestamp information
+- 🌍 **Globally unique**: No collision risk across distributed systems
+- 📊 **Database friendly**: Better index performance than random UUIDs (v4)
+- 📈 **Scalability**: No need for centralized ID generation
+- 🔀 **Merge-friendly**: Databases can be merged without ID conflicts
+
+**Implementation**:
+```go
+import "github.com/google/uuid"
+
+user := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: input.Name,
+ Email: input.Email,
+ Password: hashedPassword,
+}
+```
+
+**Database Storage**:
+- **PostgreSQL**: `UUID` type (native support)
+- **MySQL**: `BINARY(16)` type (16-byte storage)
+
+## 📋 Data Transfer Objects (DTOs)
+
+The project enforces clear boundaries between internal domain models and external API contracts.
+
+**Domain Models** (`internal/user/domain/user.go`):
+- Pure internal representation of business entities
+- No JSON tags - completely decoupled from API serialization
+- Focus on business rules and domain logic
+
+**DTOs** (`internal/user/http/dto/`):
+- `request.go` - API request structures with validation
+- `response.go` - API response structures with JSON tags
+- `mapper.go` - Conversion functions between DTOs and domain models
+
+**Benefits**:
+1. 🔒 **Separation of Concerns** - Domain models evolve independently from API contracts
+2. 🛡️ **Security** - Sensitive fields (like passwords) never exposed in API responses
+3. 🔄 **Flexibility** - Different API views of same domain model
+4. 📚 **Versioning** - Easy to maintain multiple API versions
+5. ✅ **Validation** - Request validation happens at DTO level before reaching domain logic
+
+## 🔄 Transaction Management
+
+The template implements a TxManager interface for handling database transactions:
+
+```go
+type TxManager interface {
+ WithTx(ctx context.Context, fn func(ctx context.Context) error) error
+}
+```
+
+**Usage**:
+```go
+err := uc.txManager.WithTx(ctx, func(ctx context.Context) error {
+ if err := uc.userRepo.Create(ctx, user); err != nil {
+ return err
+ }
+ if err := uc.outboxRepo.Create(ctx, event); err != nil {
+ return err
+ }
+ return nil
+})
+```
+
+The transaction is automatically injected into the context and used by repositories via `database.GetTx()`.
+
+## 📤 Transactional Outbox Pattern
+
+The project demonstrates the transactional outbox pattern for reliable event delivery:
+
+1. 📝 Business operation (e.g., user creation) is executed
+2. 📬 Event is stored in outbox table in **same transaction**
+3. 🚀 Background worker picks up pending events
+4. ✅ Events are marked as processed or failed
+
+**Benefits**:
+- 🔒 **Guaranteed delivery** - Events never lost due to transaction rollback
+- 🔁 **At-least-once delivery** - Events processed at least once
+- 🎯 **Consistency** - Business operations and events always in sync
+
+**Example (User Registration)**:
+```go
+err = uc.txManager.WithTx(ctx, func(ctx context.Context) error {
+ // Create user
+ if err := uc.userRepo.Create(ctx, user); err != nil {
+ return err
+ }
+
+ // Create event in same transaction
+ event := &outboxDomain.OutboxEvent{
+ ID: uuid.Must(uuid.NewV7()),
+ EventType: "user.created",
+ Payload: userPayload,
+ Status: outboxDomain.StatusPending,
+ }
+ return uc.outboxRepo.Create(ctx, event)
+})
+```
+
+The worker (`internal/worker/event_worker.go`) processes these events asynchronously.
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 0000000..f05082a
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,538 @@
+# 🛠️ Development Guide
+
+This guide covers the development workflow, coding standards, and best practices for this Go project.
+
+## 🔨 Build Commands
+
+### Building the Application
+
+```bash
+make build # Build the application binary
+```
+
+The binary will be created at `./bin/app`.
+
+### Running Commands
+
+```bash
+make run-server # Build and run HTTP server
+make run-worker # Build and run background worker
+make run-migrate # Build and run database migrations
+```
+
+### Other Commands
+
+```bash
+make clean # Remove build artifacts and coverage files
+make deps # Download and tidy dependencies
+make help # Show all available make targets
+```
+
+## 🎨 Code Style Guidelines
+
+### Import Organization
+
+Follow this import grouping order (enforced by `goimports`):
+
+1. **Standard library packages**
+2. **External packages** (third-party)
+3. **Local project packages** (prefixed with your module path)
+
+**Example**:
+```go
+import (
+ "context"
+ "database/sql"
+ "strings"
+
+ "github.com/google/uuid"
+ validation "github.com/jellydator/validation"
+
+ "github.com/allisson/go-project-template/internal/database"
+ "github.com/allisson/go-project-template/internal/errors"
+ "github.com/allisson/go-project-template/internal/user/domain"
+)
+```
+
+**Import Aliases**:
+When renaming imports, use descriptive aliases:
+- `apperrors` for `internal/errors`
+- `appValidation` for `internal/validation`
+- `outboxDomain` for `internal/outbox/domain`
+
+### Formatting
+
+- **Line length**: Max 110 characters (enforced by `golines`)
+- **Indentation**: Use tabs (tab-len: 4)
+- **Comments**: Not shortened by golines
+- Run `make lint` before committing to auto-fix formatting issues
+
+### Naming Conventions
+
+#### Packages
+- Use lowercase, single-word names when possible
+- Avoid underscores or mixed caps
+- ✅ Good: `domain`, `usecase`, `repository`, `http`
+- ❌ Bad: `user_domain`, `userDomain`, `UserDomain`
+
+#### Types
+- Use PascalCase for exported types
+- Use camelCase for unexported types
+- Suffix interfaces with meaningful names
+- ✅ Good: `UserRepository`, `TxManager`, `UseCase`
+- ❌ Bad: `UserRepositoryInterface`, `IUserRepository`
+
+#### Functions/Methods
+- Use PascalCase for exported functions
+- Use camelCase for unexported functions
+- Prefix boolean functions with `is`, `has`, `can`, or `should`
+- ✅ Good: `isPostgreSQLUniqueViolation`, `hasUpperCase`
+
+#### Variables
+- Use camelCase for short-lived variables
+- Use descriptive names for longer-lived variables
+- Avoid single-letter names except for:
+ - `i`, `j`, `k` (loops)
+ - `r` (receiver)
+ - `w` (http.ResponseWriter)
+ - `ctx` (context)
+
+#### Constants
+- Use PascalCase for exported constants
+- Use camelCase for unexported constants
+
+## 🔍 Linting
+
+### Running the Linter
+
+```bash
+make lint # Run golangci-lint with auto-fix
+```
+
+Or directly:
+```bash
+golangci-lint run -v --fix # Direct linter command
+```
+
+The linter will:
+- ✅ Check code formatting (goimports, golines)
+- ✅ Detect potential bugs
+- ✅ Enforce code style guidelines
+- ✅ Check for common mistakes
+- ✅ Automatically fix issues when possible
+
+### Linter Configuration
+
+The project uses `.golangci.yml` for linter configuration. Key settings:
+
+```yaml
+linters:
+ enable:
+ - goimports # Auto-organize imports
+ - golines # Enforce line length
+ - errcheck # Check error handling
+ - gosimple # Simplify code
+ - govet # Report suspicious constructs
+ - staticcheck # Static analysis
+```
+
+## 📝 Comments and Documentation
+
+### Package Comments
+
+Every package should have a doc comment:
+
+```go
+// Package usecase implements the user business logic and orchestrates user domain operations.
+package usecase
+```
+
+### Exported Types
+
+Document all exported types, functions, and constants:
+
+```go
+// User represents a user in the system
+type User struct { ... }
+
+// RegisterUser registers a new user and creates a user.created event
+func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
+ // Implementation
+}
+```
+
+### Implementation Comments
+
+Add comments for complex logic:
+
+```go
+// Hash the password using Argon2id
+hashedPassword, err := uc.passwordHasher.Hash([]byte(input.Password))
+```
+
+## 🗄️ Database Patterns
+
+### Repository Pattern
+
+**Key Principles**:
+- ✅ Implement separate repositories for MySQL and PostgreSQL
+- ✅ Use `database.GetTx(ctx, r.db)` to get querier (supports transactions)
+- ✅ Use numbered placeholders for PostgreSQL (`$1, $2`) and `?` for MySQL
+- ✅ Transform infrastructure errors to domain errors
+
+**Example**:
+```go
+func (r *PostgreSQLUserRepository) Create(ctx context.Context, user *domain.User) error {
+ querier := database.GetTx(ctx, r.db)
+
+ query := `INSERT INTO users (id, name, email, password, created_at, updated_at)
+ VALUES ($1, $2, $3, $4, NOW(), NOW())`
+
+ _, err := querier.ExecContext(ctx, query, user.ID, user.Name, user.Email, user.Password)
+ if err != nil {
+ if isPostgreSQLUniqueViolation(err) {
+ return domain.ErrUserAlreadyExists
+ }
+ return apperrors.Wrap(err, "failed to create user")
+ }
+ return nil
+}
+```
+
+### Unique Constraint Violations
+
+Check for database-specific error patterns:
+
+**PostgreSQL**:
+```go
+func isPostgreSQLUniqueViolation(err error) bool {
+ return strings.Contains(err.Error(), "duplicate key") ||
+ strings.Contains(err.Error(), "unique constraint")
+}
+```
+
+**MySQL**:
+```go
+func isMySQLUniqueViolation(err error) bool {
+ return strings.Contains(err.Error(), "Error 1062") ||
+ strings.Contains(err.Error(), "Duplicate entry")
+}
+```
+
+## ✅ Validation Patterns
+
+### Using jellydator/validation
+
+```go
+import (
+ validation "github.com/jellydator/validation"
+ appValidation "github.com/allisson/go-project-template/internal/validation"
+)
+
+func (r *RegisterUserRequest) Validate() error {
+ err := validation.ValidateStruct(r,
+ validation.Field(&r.Name,
+ validation.Required.Error("name is required"),
+ appValidation.NotBlank,
+ validation.Length(1, 255),
+ ),
+ validation.Field(&r.Email,
+ validation.Required.Error("email is required"),
+ appValidation.Email,
+ ),
+ validation.Field(&r.Password,
+ validation.Required.Error("password is required"),
+ appValidation.PasswordStrength{
+ MinLength: 8,
+ RequireUpper: true,
+ RequireLower: true,
+ RequireNumber: true,
+ RequireSpecial: true,
+ },
+ ),
+ )
+ return appValidation.WrapValidationError(err)
+}
+```
+
+### Custom Validation Rules
+
+The project provides reusable validation rules in `internal/validation`:
+
+- `Email` - Email format validation
+- `NotBlank` - Not empty after trimming
+- `NoWhitespace` - No leading/trailing whitespace
+- `PasswordStrength` - Configurable password requirements
+
+## 🆔 Working with UUIDs
+
+### Generating UUIDs
+
+Always use UUIDv7 for primary keys:
+
+```go
+import "github.com/google/uuid"
+
+user := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: input.Name,
+ Email: input.Email,
+ Password: hashedPassword,
+}
+```
+
+### Database Storage
+
+**PostgreSQL** - Native UUID type:
+```sql
+CREATE TABLE users (
+ id UUID PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) UNIQUE NOT NULL
+);
+```
+
+**MySQL** - BINARY(16) with marshal/unmarshal:
+```sql
+CREATE TABLE users (
+ id BINARY(16) PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) UNIQUE NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+```go
+// MySQL requires UUID marshaling
+uuidBytes, err := user.ID.MarshalBinary()
+if err != nil {
+ return apperrors.Wrap(err, "failed to marshal UUID")
+}
+_, err = querier.ExecContext(ctx, query, uuidBytes, user.Name, user.Email)
+```
+
+## 🔄 Transaction Management
+
+### Using TxManager
+
+```go
+err := uc.txManager.WithTx(ctx, func(ctx context.Context) error {
+ // All operations in this function share the same transaction
+
+ if err := uc.userRepo.Create(ctx, user); err != nil {
+ return err // Transaction will rollback
+ }
+
+ if err := uc.outboxRepo.Create(ctx, event); err != nil {
+ return err // Transaction will rollback
+ }
+
+ return nil // Transaction will commit
+})
+```
+
+### Repository Support for Transactions
+
+Repositories automatically use transactions when available:
+
+```go
+func (r *PostgreSQLUserRepository) Create(ctx context.Context, user *domain.User) error {
+ // GetTx returns transaction if present in context, otherwise returns DB
+ querier := database.GetTx(ctx, r.db)
+
+ _, err := querier.ExecContext(ctx, query, args...)
+ return err
+}
+```
+
+## 📦 Dependency Injection
+
+### Adding Components to DI Container
+
+When adding new components to `internal/app/di.go`:
+
+1. **Add fields** to Container struct:
+```go
+type Container struct {
+ // ... existing fields
+ productRepo productUsecase.ProductRepository
+ productUseCase productUsecase.UseCase // Interface!
+ productRepoInit sync.Once
+ productUseCaseInit sync.Once
+}
+```
+
+2. **Add getter methods** with lazy initialization:
+```go
+func (c *Container) ProductRepository() (productUsecase.ProductRepository, error) {
+ var err error
+ c.productRepoInit.Do(func() {
+ c.productRepo, err = c.initProductRepository()
+ if err != nil {
+ c.initErrors["productRepo"] = err
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+ if existingErr, ok := c.initErrors["productRepo"]; ok {
+ return nil, existingErr
+ }
+ return c.productRepo, nil
+}
+```
+
+3. **Add initialization methods**:
+```go
+func (c *Container) initProductRepository() (productUsecase.ProductRepository, error) {
+ db, err := c.DB()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get database: %w", err)
+ }
+
+ switch c.config.DBDriver {
+ case "mysql":
+ return productRepository.NewMySQLProductRepository(db), nil
+ case "postgres":
+ return productRepository.NewPostgreSQLProductRepository(db), nil
+ default:
+ return nil, fmt.Errorf("unsupported database driver: %s", c.config.DBDriver)
+ }
+}
+```
+
+## 🚀 Development Workflow
+
+### 1. Create a New Feature Branch
+
+```bash
+git checkout -b feature/your-feature-name
+```
+
+### 2. Make Changes
+
+- Write code following the style guidelines
+- Add tests for new functionality
+- Update documentation as needed
+
+### 3. Run Tests
+
+```bash
+make test-db-up # Start test databases
+make test # Run tests
+make test-db-down # Stop test databases
+```
+
+Or use the combined command:
+```bash
+make test-with-db # Start databases, run tests, stop databases
+```
+
+### 4. Run Linter
+
+```bash
+make lint
+```
+
+### 5. Build the Application
+
+```bash
+make build
+```
+
+### 6. Commit Your Changes
+
+```bash
+git add .
+git commit -m "feat: add new feature"
+```
+
+**Commit Message Format**:
+- `feat:` - New feature
+- `fix:` - Bug fix
+- `docs:` - Documentation changes
+- `refactor:` - Code refactoring
+- `test:` - Test updates
+- `chore:` - Maintenance tasks
+
+### 7. Push and Create Pull Request
+
+```bash
+git push origin feature/your-feature-name
+```
+
+Then create a pull request on GitHub.
+
+## 🔧 Common Development Tasks
+
+### Add a New Endpoint
+
+1. Define request/response DTOs in `internal/{domain}/http/dto/`
+2. Add validation to request DTO
+3. Create handler in `internal/{domain}/http/{domain}_handler.go`
+4. Register route in `internal/http/server.go`
+5. Add tests
+
+### Add a New Use Case
+
+1. Define use case method in interface (`internal/{domain}/usecase/`)
+2. Implement business logic
+3. Add validation
+4. Handle transactions if needed
+5. Add tests
+
+### Create a Database Migration
+
+1. Create `000xxx_description.up.sql` in `migrations/postgresql/` or `migrations/mysql/`
+2. Create corresponding `000xxx_description.down.sql`
+3. Run migrations: `make run-migrate`
+
+### Add a Custom Validation Rule
+
+1. Add rule to `internal/validation/rules.go`
+2. Implement `Validate(value interface{}) error` method
+3. Add tests in `internal/validation/rules_test.go`
+4. Use in DTOs
+
+## 📚 Useful Resources
+
+- [Effective Go](https://golang.org/doc/effective_go)
+- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
+- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
+- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
+
+## 🐛 Debugging Tips
+
+### Enable Debug Logging
+
+Set `LOG_LEVEL=debug` in your `.env` file:
+
+```bash
+LOG_LEVEL=debug
+```
+
+### Database Query Debugging
+
+Add logging in repositories to see SQL queries:
+
+```go
+log.Printf("Executing query: %s with args: %v", query, args)
+```
+
+### Use Delve Debugger
+
+Install Delve:
+```bash
+go install github.com/go-delve/delve/cmd/dlv@latest
+```
+
+Run with debugger:
+```bash
+dlv debug ./cmd/app -- server
+```
+
+### Check Health Endpoints
+
+```bash
+curl http://localhost:8080/health # Application health
+curl http://localhost:8080/ready # Database connectivity
+```
diff --git a/docs/error-handling.md b/docs/error-handling.md
new file mode 100644
index 0000000..d2eb3cf
--- /dev/null
+++ b/docs/error-handling.md
@@ -0,0 +1,503 @@
+# ⚠️ Error Handling
+
+This guide explains the standardized error handling system used in this Go project.
+
+## 🎯 Philosophy
+
+The error handling system is designed to:
+
+- 🎯 **Express business intent** rather than expose infrastructure details
+- 🔒 **Prevent information leaks** (database errors never exposed to API clients)
+- ✅ **Maintain consistency** (same error always maps to same HTTP status)
+- 🔍 **Enable type-safe error checking** using `errors.Is()`
+- 📊 **Provide structured responses** with consistent JSON format
+
+## 📚 Standard Domain Errors
+
+The project defines standard domain errors in `internal/errors/errors.go`:
+
+```go
+package errors
+
+import "errors"
+
+var (
+ ErrNotFound = errors.New("not found") // 404 Not Found
+ ErrConflict = errors.New("conflict") // 409 Conflict
+ ErrInvalidInput = errors.New("invalid input") // 422 Unprocessable Entity
+ ErrUnauthorized = errors.New("unauthorized") // 401 Unauthorized
+ ErrForbidden = errors.New("forbidden") // 403 Forbidden
+)
+
+// Wrap wraps an error with additional context
+func Wrap(err error, message string) error {
+ return fmt.Errorf("%s: %w", message, err)
+}
+```
+
+## 🏷️ Domain-Specific Errors
+
+Each domain defines its own specific errors by **wrapping** the standard errors:
+
+```go
+// internal/user/domain/user.go
+package domain
+
+import (
+ apperrors "github.com/allisson/go-project-template/internal/errors"
+)
+
+var (
+ ErrUserNotFound = apperrors.Wrap(apperrors.ErrNotFound, "user not found")
+ ErrUserAlreadyExists = apperrors.Wrap(apperrors.ErrConflict, "user already exists")
+ ErrInvalidEmail = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid email format")
+ ErrWeakPassword = apperrors.Wrap(apperrors.ErrInvalidInput, "password does not meet strength requirements")
+)
+```
+
+**Key Points**:
+- ✅ Wrap standard errors with domain-specific context
+- ✅ Use descriptive error messages that explain the business problem
+- ✅ Group related errors in the domain package
+
+## 🔄 Error Flow Through Layers
+
+Errors flow through the application layers, being transformed from infrastructure concerns to domain concepts:
+
+```
+Infrastructure Error (sql.ErrNoRows)
+ ↓
+ [Repository Layer]
+ ↓
+Domain Error (ErrUserNotFound)
+ ↓
+ [Use Case Layer]
+ ↓
+Domain Error (unchanged)
+ ↓
+ [HTTP Handler Layer]
+ ↓
+HTTP Response (404 Not Found)
+```
+
+### 1️⃣ Repository Layer
+
+**Responsibility**: Transform infrastructure errors to domain errors
+
+```go
+// internal/user/repository/postgresql_user_repository.go
+func (r *PostgreSQLUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
+ querier := database.GetTx(ctx, r.db)
+ query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1`
+
+ var user domain.User
+ err := querier.QueryRowContext(ctx, query, id).Scan(
+ &user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, domain.ErrUserNotFound // ✅ Transform infrastructure → domain
+ }
+ return nil, apperrors.Wrap(err, "failed to get user by id")
+ }
+ return &user, nil
+}
+```
+
+**Unique Constraint Violations**:
+
+```go
+func (r *PostgreSQLUserRepository) Create(ctx context.Context, user *domain.User) error {
+ querier := database.GetTx(ctx, r.db)
+
+ query := `INSERT INTO users (id, name, email, password, created_at, updated_at)
+ VALUES ($1, $2, $3, $4, NOW(), NOW())`
+
+ _, err := querier.ExecContext(ctx, query, user.ID, user.Name, user.Email, user.Password)
+ if err != nil {
+ if isPostgreSQLUniqueViolation(err) {
+ return domain.ErrUserAlreadyExists // ✅ Transform unique violation → domain error
+ }
+ return apperrors.Wrap(err, "failed to create user")
+ }
+ return nil
+}
+
+func isPostgreSQLUniqueViolation(err error) bool {
+ return strings.Contains(err.Error(), "duplicate key") ||
+ strings.Contains(err.Error(), "unique constraint")
+}
+```
+
+**MySQL Example**:
+
+```go
+func isMySQLUniqueViolation(err error) bool {
+ return strings.Contains(err.Error(), "Error 1062") ||
+ strings.Contains(err.Error(), "Duplicate entry")
+}
+```
+
+### 2️⃣ Use Case Layer
+
+**Responsibility**: Return domain errors directly (don't wrap again)
+
+```go
+// internal/user/usecase/user_usecase.go
+func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
+ // Validate input
+ if err := input.Validate(); err != nil {
+ return nil, err // ✅ Return validation error directly
+ }
+
+ // Check if user exists
+ existingUser, err := uc.userRepo.GetByEmail(ctx, input.Email)
+ if err != nil && !errors.Is(err, domain.ErrUserNotFound) {
+ return nil, err // ✅ Return unexpected errors directly
+ }
+ if existingUser != nil {
+ return nil, domain.ErrUserAlreadyExists // ✅ Return domain error directly
+ }
+
+ // Hash password
+ hashedPassword, err := uc.passwordHasher.Hash([]byte(input.Password))
+ if err != nil {
+ return nil, apperrors.Wrap(err, "failed to hash password")
+ }
+
+ // Create user
+ user := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: input.Name,
+ Email: input.Email,
+ Password: string(hashedPassword),
+ }
+
+ err = uc.txManager.WithTx(ctx, func(ctx context.Context) error {
+ if err := uc.userRepo.Create(ctx, user); err != nil {
+ return err // ✅ Pass through domain errors
+ }
+ return uc.outboxRepo.Create(ctx, event)
+ })
+
+ return user, err // ✅ Return error unchanged
+}
+```
+
+**Key Points**:
+- ✅ **Never wrap domain errors** in the use case layer
+- ✅ **Return domain errors directly** to maintain error chain
+- ✅ **Use `errors.Is()`** to check for specific error types
+
+### 3️⃣ HTTP Handler Layer
+
+**Responsibility**: Map domain errors to HTTP responses
+
+```go
+// internal/user/http/user_handler.go
+func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
+ var req dto.RegisterUserRequest
+
+ // Decode request
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ httputil.HandleValidationError(w, err, h.logger) // 400 Bad Request
+ return
+ }
+
+ // Validate request
+ if err := req.Validate(); err != nil {
+ httputil.HandleError(w, err, h.logger) // 422 Unprocessable Entity
+ return
+ }
+
+ // Execute use case
+ input := dto.ToRegisterUserInput(req)
+ user, err := h.userUseCase.RegisterUser(r.Context(), input)
+ if err != nil {
+ httputil.HandleError(w, err, h.logger) // ✅ Auto-maps to HTTP status
+ return
+ }
+
+ // Success response
+ response := dto.ToUserResponse(user)
+ httputil.MakeJSONResponse(w, http.StatusCreated, response)
+}
+```
+
+## 🌐 HTTP Error Mapping
+
+The `httputil.HandleError()` function automatically maps domain errors to HTTP status codes:
+
+```go
+// internal/httputil/response.go
+func HandleError(w http.ResponseWriter, err error, logger *slog.Logger) {
+ var statusCode int
+ var errorCode string
+ var message string
+
+ switch {
+ case errors.Is(err, apperrors.ErrNotFound):
+ statusCode = http.StatusNotFound
+ errorCode = "not_found"
+ message = "The requested resource was not found"
+ case errors.Is(err, apperrors.ErrConflict):
+ statusCode = http.StatusConflict
+ errorCode = "conflict"
+ message = "A conflict occurred with existing data"
+ case errors.Is(err, apperrors.ErrInvalidInput):
+ statusCode = http.StatusUnprocessableEntity
+ errorCode = "invalid_input"
+ message = err.Error() // Include detailed validation message
+ case errors.Is(err, apperrors.ErrUnauthorized):
+ statusCode = http.StatusUnauthorized
+ errorCode = "unauthorized"
+ message = "Authentication is required"
+ case errors.Is(err, apperrors.ErrForbidden):
+ statusCode = http.StatusForbidden
+ errorCode = "forbidden"
+ message = "You don't have permission to access this resource"
+ default:
+ statusCode = http.StatusInternalServerError
+ errorCode = "internal_error"
+ message = "An internal error occurred"
+ }
+
+ // Log error with context
+ logger.Error("request error",
+ "error", err.Error(),
+ "status_code", statusCode,
+ "error_code", errorCode,
+ )
+
+ // Send JSON response
+ MakeJSONResponse(w, statusCode, map[string]string{
+ "error": errorCode,
+ "message": message,
+ })
+}
+```
+
+### Error to HTTP Status Mapping Table
+
+| Domain Error | HTTP Status | Error Code | Use Case |
+|--------------|-------------|------------|----------|
+| `ErrNotFound` | 404 | `not_found` | Resource doesn't exist |
+| `ErrConflict` | 409 | `conflict` | Duplicate or conflicting data |
+| `ErrInvalidInput` | 422 | `invalid_input` | Validation failures |
+| `ErrUnauthorized` | 401 | `unauthorized` | Authentication required |
+| `ErrForbidden` | 403 | `forbidden` | Permission denied |
+| Unknown | 500 | `internal_error` | Unexpected errors |
+
+## 📋 Error Response Format
+
+All error responses follow a consistent JSON structure:
+
+```json
+{
+ "error": "error_code",
+ "message": "Human-readable error message"
+}
+```
+
+### Examples
+
+**404 Not Found**:
+```json
+{
+ "error": "not_found",
+ "message": "The requested resource was not found"
+}
+```
+
+**409 Conflict**:
+```json
+{
+ "error": "conflict",
+ "message": "A conflict occurred with existing data"
+}
+```
+
+**422 Validation Error**:
+```json
+{
+ "error": "invalid_input",
+ "message": "email: must be a valid email address; password: password must contain at least one uppercase letter."
+}
+```
+
+**500 Internal Server Error**:
+```json
+{
+ "error": "internal_error",
+ "message": "An internal error occurred"
+}
+```
+
+## ✅ Validation Errors
+
+Validation errors are treated as `ErrInvalidInput` and automatically return 422 status.
+
+### DTO Validation
+
+```go
+// internal/user/http/dto/request.go
+func (r *RegisterUserRequest) Validate() error {
+ err := validation.ValidateStruct(r,
+ validation.Field(&r.Name,
+ validation.Required.Error("name is required"),
+ appValidation.NotBlank,
+ validation.Length(1, 255),
+ ),
+ validation.Field(&r.Email,
+ validation.Required.Error("email is required"),
+ appValidation.Email,
+ ),
+ validation.Field(&r.Password,
+ validation.Required.Error("password is required"),
+ appValidation.PasswordStrength{
+ MinLength: 8,
+ RequireUpper: true,
+ RequireLower: true,
+ RequireNumber: true,
+ RequireSpecial: true,
+ },
+ ),
+ )
+ return appValidation.WrapValidationError(err) // ✅ Wraps as ErrInvalidInput
+}
+```
+
+### WrapValidationError
+
+The `WrapValidationError` function converts validation errors to `ErrInvalidInput`:
+
+```go
+// internal/validation/rules.go
+func WrapValidationError(err error) error {
+ if err == nil {
+ return nil
+ }
+ return apperrors.Wrap(apperrors.ErrInvalidInput, err.Error())
+}
+```
+
+## 🔍 Checking Errors
+
+Use `errors.Is()` to check for specific error types:
+
+```go
+user, err := uc.userRepo.GetByEmail(ctx, email)
+if err != nil {
+ if errors.Is(err, domain.ErrUserNotFound) {
+ // Handle not found case
+ return nil, domain.ErrInvalidCredentials
+ }
+ // Handle other errors
+ return nil, err
+}
+```
+
+**DO ✅**:
+```go
+if errors.Is(err, domain.ErrUserNotFound) {
+ // Handle error
+}
+```
+
+**DON'T ❌**:
+```go
+if err == domain.ErrUserNotFound { // Won't work with wrapped errors
+ // Handle error
+}
+```
+
+## ➕ Adding Errors to New Domains
+
+When creating a new domain (e.g., `product`), define domain-specific errors:
+
+```go
+// internal/product/domain/product.go
+package domain
+
+import (
+ apperrors "github.com/allisson/go-project-template/internal/errors"
+)
+
+var (
+ ErrProductNotFound = apperrors.Wrap(apperrors.ErrNotFound, "product not found")
+ ErrInsufficientStock = apperrors.Wrap(apperrors.ErrConflict, "insufficient stock")
+ ErrInvalidPrice = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid price")
+ ErrInvalidQuantity = apperrors.Wrap(apperrors.ErrInvalidInput, "quantity must be positive")
+)
+```
+
+Then use `httputil.HandleError()` in your HTTP handlers for automatic mapping - no additional code needed!
+
+## 🎯 Best Practices
+
+### DO ✅
+
+- ✅ **Define domain-specific errors** by wrapping standard errors
+- ✅ **Transform infrastructure errors** in repository layer
+- ✅ **Return domain errors directly** from use case layer
+- ✅ **Use `errors.Is()`** for type-safe error checking
+- ✅ **Use `httputil.HandleError()`** for consistent HTTP error responses
+- ✅ **Log errors with context** before responding to client
+- ✅ **Include descriptive messages** in domain errors
+- ✅ **Keep error messages user-friendly** (no stack traces or internal details)
+
+### DON'T ❌
+
+- ❌ **Don't expose infrastructure errors** (like `sql.ErrNoRows`) to API clients
+- ❌ **Don't wrap domain errors** multiple times in use case layer
+- ❌ **Don't compare errors with `==`** (use `errors.Is()` instead)
+- ❌ **Don't return different HTTP status codes** for the same domain error
+- ❌ **Don't include sensitive information** in error messages
+- ❌ **Don't return stack traces** to API clients
+- ❌ **Don't create new error types** when standard errors suffice
+
+## 🔒 Security Considerations
+
+### Information Disclosure
+
+**DO ✅**:
+```go
+// Return generic error message
+return domain.ErrUserNotFound
+```
+
+**DON'T ❌**:
+```go
+// Reveals internal database structure
+return fmt.Errorf("no row found in users table for id=%s", id)
+```
+
+### Error Logging
+
+Log detailed errors on the server, but return generic messages to clients:
+
+```go
+func HandleError(w http.ResponseWriter, err error, logger *slog.Logger) {
+ // Log detailed error server-side
+ logger.Error("request error",
+ "error", err.Error(),
+ "stack", getStackTrace(),
+ )
+
+ // Return generic message to client
+ MakeJSONResponse(w, statusCode, map[string]string{
+ "error": errorCode,
+ "message": genericMessage, // No sensitive details
+ })
+}
+```
+
+## 📊 Benefits Summary
+
+1. 🎯 **No Infrastructure Leaks** - Database errors never exposed to API clients
+2. 💼 **Business Intent** - Errors express domain concepts (e.g., `ErrUserNotFound` vs `sql.ErrNoRows`)
+3. ✅ **Consistent HTTP Mapping** - Same domain error always maps to same HTTP status
+4. 🔒 **Type-Safe** - Use `errors.Is()` to check for specific error types
+5. 📋 **Structured Responses** - All errors return consistent JSON format
+6. 📊 **Centralized Logging** - All errors logged with full context before responding
+7. 🛡️ **Security** - Sensitive information never leaked in error messages
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..920c8b9
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,348 @@
+# 🚀 Getting Started
+
+This guide will help you set up and run the Go project template on your local machine.
+
+## 📋 Prerequisites
+
+Before you begin, ensure you have the following installed:
+
+- ✅ **Go 1.25 or higher** - [Download Go](https://golang.org/dl/)
+- ✅ **PostgreSQL 12+** or **MySQL 8.0+** - For development
+- ✅ **Docker and Docker Compose** - For testing and optional development
+- ✅ **Make** (optional) - For convenience commands
+
+## 📥 Installation
+
+### 1. Clone the repository
+
+```bash
+git clone https://github.com/allisson/go-project-template.git
+cd go-project-template
+```
+
+### 2. Customize the module path
+
+After cloning, update the import paths to match your project.
+
+#### Option 1: Using find and sed (Linux/macOS)
+
+```bash
+# Replace with your actual module path
+NEW_MODULE="github.com/yourname/yourproject"
+
+# Update go.mod
+sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" go.mod
+
+# Update all Go files
+find . -type f -name "*.go" -exec sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" {} +
+```
+
+#### Option 2: Using PowerShell (Windows)
+
+```powershell
+# Replace with your actual module path
+$NEW_MODULE = "github.com/yourname/yourproject"
+
+# Update go.mod
+(Get-Content go.mod) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content go.mod
+
+# Update all Go files
+Get-ChildItem -Recurse -Filter *.go | ForEach-Object {
+ (Get-Content $_.FullName) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content $_.FullName
+}
+```
+
+#### Option 3: Manually
+
+1. Update the module name in `go.mod`
+2. Search and replace `github.com/allisson/go-project-template` with your module path in all `.go` files
+
+After updating, verify the changes and tidy dependencies:
+
+```bash
+go mod tidy
+```
+
+**Important**: Also update the `.golangci.yml` file to match your new module path:
+
+```yaml
+formatters:
+ settings:
+ goimports:
+ local-prefixes:
+ - github.com/yourname/yourproject # Update this line
+```
+
+This ensures the linter correctly groups your local imports.
+
+### 3. Install dependencies
+
+```bash
+go mod download
+```
+
+## ⚙️ Configuration
+
+### Environment Variables
+
+The application automatically loads environment variables from a `.env` file. Create a `.env` file in your project root:
+
+```bash
+# Database configuration
+DB_DRIVER=postgres # or mysql
+DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
+DB_MAX_OPEN_CONNECTIONS=25
+DB_MAX_IDLE_CONNECTIONS=5
+DB_CONN_MAX_LIFETIME=5
+
+# Server configuration
+SERVER_HOST=0.0.0.0
+SERVER_PORT=8080
+
+# Logging
+LOG_LEVEL=info
+
+# Worker configuration
+WORKER_INTERVAL=5
+WORKER_BATCH_SIZE=10
+WORKER_MAX_RETRIES=3
+WORKER_RETRY_INTERVAL=1
+```
+
+**Note**: The application searches for the `.env` file recursively from the current working directory up to the root directory. This allows you to run the application from any subdirectory.
+
+### Configuration Options
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `SERVER_HOST` | HTTP server host | `0.0.0.0` |
+| `SERVER_PORT` | HTTP server port | `8080` |
+| `DB_DRIVER` | Database driver (`postgres`/`mysql`) | `postgres` |
+| `DB_CONNECTION_STRING` | Database connection string | - |
+| `DB_MAX_OPEN_CONNECTIONS` | Max open connections | `25` |
+| `DB_MAX_IDLE_CONNECTIONS` | Max idle connections | `5` |
+| `DB_CONN_MAX_LIFETIME` | Connection max lifetime (minutes) | `5` |
+| `LOG_LEVEL` | Log level (`debug`/`info`/`warn`/`error`) | `info` |
+| `WORKER_INTERVAL` | Worker poll interval (seconds) | `5` |
+| `WORKER_BATCH_SIZE` | Events to process per batch | `10` |
+| `WORKER_MAX_RETRIES` | Max retry attempts | `3` |
+| `WORKER_RETRY_INTERVAL` | Retry interval (seconds) | `1` |
+
+## 🗄️ Database Setup
+
+### Using Docker (Recommended)
+
+#### PostgreSQL
+
+```bash
+make dev-postgres
+```
+
+This starts PostgreSQL on port `5432` with the following credentials:
+- **User**: `postgres`
+- **Password**: `postgres`
+- **Database**: `mydb`
+
+#### MySQL
+
+```bash
+make dev-mysql
+```
+
+This starts MySQL on port `3306` with the following credentials:
+- **User**: `root`
+- **Password**: `root`
+- **Database**: `mydb`
+
+### Using Local Installation
+
+If you have PostgreSQL or MySQL installed locally, create a database and update your `.env` file with the appropriate connection string.
+
+**PostgreSQL**:
+```sql
+CREATE DATABASE mydb;
+```
+
+**MySQL**:
+```sql
+CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+```
+
+## 🔄 Database Migrations
+
+Run database migrations to create the required tables:
+
+```bash
+make run-migrate
+```
+
+This command:
+1. Connects to your database using the `DB_CONNECTION_STRING`
+2. Runs all pending migrations from the appropriate directory (`migrations/postgresql` or `migrations/mysql`)
+3. Creates the `users` and `outbox_events` tables
+
+## ▶️ Running the Application
+
+### Start the HTTP Server
+
+```bash
+make run-server
+```
+
+The server will be available at http://localhost:8080
+
+**Health Check**:
+```bash
+curl http://localhost:8080/health
+```
+
+**Readiness Check**:
+```bash
+curl http://localhost:8080/ready
+```
+
+### Start the Background Worker
+
+In another terminal, start the worker process:
+
+```bash
+make run-worker
+```
+
+The worker processes outbox events from the database.
+
+## 🧪 Testing the API
+
+### Register a User
+
+```bash
+curl -X POST http://localhost:8080/api/users \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "John Doe",
+ "email": "john@example.com",
+ "password": "SecurePass123!"
+ }'
+```
+
+**Success Response** (201 Created):
+```json
+{
+ "id": "01936a99-8c2f-7890-b123-456789abcdef",
+ "name": "John Doe",
+ "email": "john@example.com",
+ "created_at": "2024-01-15T10:30:00Z",
+ "updated_at": "2024-01-15T10:30:00Z"
+}
+```
+
+**Password Requirements**:
+- ✅ Minimum 8 characters
+- ✅ At least one uppercase letter
+- ✅ At least one lowercase letter
+- ✅ At least one number
+- ✅ At least one special character
+
+**Validation Error Response** (422 Unprocessable Entity):
+```json
+{
+ "error": "invalid_input",
+ "message": "email: must be a valid email address; password: password must contain at least one uppercase letter."
+}
+```
+
+## 🐳 Docker Deployment
+
+### Build Docker Image
+
+```bash
+make docker-build
+```
+
+### Run Server in Docker
+
+```bash
+make docker-run-server
+```
+
+### Run Worker in Docker
+
+```bash
+make docker-run-worker
+```
+
+### Run Migrations in Docker
+
+```bash
+make docker-run-migrate
+```
+
+## 🔧 CLI Commands
+
+The binary supports three commands via `urfave/cli`:
+
+### Start HTTP Server
+```bash
+./bin/app server
+```
+
+### Run Database Migrations
+```bash
+./bin/app migrate
+```
+
+### Run Event Worker
+```bash
+./bin/app worker
+```
+
+## 📚 Next Steps
+
+Now that you have the application running, you can:
+
+- 📖 Learn about the [Architecture](architecture.md)
+- 🛠️ Set up your [Development Environment](development.md)
+- ✅ Learn about [Testing](testing.md)
+- ⚠️ Understand [Error Handling](error-handling.md)
+- ➕ Learn how to [Add New Domains](adding-domains.md)
+
+## 🆘 Troubleshooting
+
+### Database Connection Issues
+
+**Problem**: `failed to connect to database`
+
+**Solutions**:
+- ✅ Verify database is running: `docker ps` (if using Docker)
+- ✅ Check connection string in `.env` file
+- ✅ Ensure database credentials are correct
+- ✅ Check firewall settings
+
+### Port Already in Use
+
+**Problem**: `bind: address already in use`
+
+**Solutions**:
+- ✅ Change `SERVER_PORT` in `.env` file
+- ✅ Stop other services using the same port
+- ✅ Use `lsof -i :8080` (Linux/macOS) or `netstat -ano | findstr :8080` (Windows) to find the process
+
+### Migration Failures
+
+**Problem**: `migration failed`
+
+**Solutions**:
+- ✅ Check database connectivity
+- ✅ Verify you're using the correct database driver (`postgres` or `mysql`)
+- ✅ Ensure database user has sufficient permissions
+- ✅ Check migration files for syntax errors
+
+### Module Import Errors
+
+**Problem**: `cannot find package`
+
+**Solutions**:
+- ✅ Run `go mod tidy` to sync dependencies
+- ✅ Verify you updated all import paths after cloning
+- ✅ Check `.golangci.yml` has correct module path
+- ✅ Clear Go cache: `go clean -modcache`
diff --git a/docs/testing.md b/docs/testing.md
new file mode 100644
index 0000000..81af0a5
--- /dev/null
+++ b/docs/testing.md
@@ -0,0 +1,511 @@
+# 🧪 Testing Guide
+
+This guide covers testing strategies, best practices, and how to write effective tests for this Go project.
+
+## 🎯 Testing Philosophy
+
+The project uses **integration testing with real databases** instead of mocks for repository layer tests.
+
+**Why Real Databases?**
+- ✅ **Accuracy** - Tests verify actual SQL queries and database behavior
+- ✅ **Real Integration** - Catches database-specific issues (constraints, types, unique violations)
+- ✅ **Production Parity** - Tests reflect real production scenarios
+- ✅ **Less Maintenance** - No mock expectations to maintain or update
+- ✅ **Confidence** - Full database integration coverage
+
+## 🏗️ Test Infrastructure
+
+### Test Databases
+
+Tests use Docker Compose to spin up isolated test databases:
+
+- **PostgreSQL**: `localhost:5433` (testuser/testpassword/testdb)
+- **MySQL**: `localhost:3307` (testuser/testpassword/testdb)
+
+**Note**: Different ports from development (5432/3306) to avoid conflicts.
+
+### Test Utilities
+
+The `testutil` package (`internal/testutil/database.go`) provides helper functions:
+
+1. **`SetupPostgresDB(t)`** - Connect to PostgreSQL and run migrations
+2. **`SetupMySQLDB(t)`** - Connect to MySQL and run migrations
+3. **`CleanupPostgresDB(t, db)`** - Clean up PostgreSQL test data
+4. **`CleanupMySQLDB(t, db)`** - Clean up MySQL test data
+5. **`TeardownDB(t, db)`** - Close database connection
+
+## 🚀 Running Tests
+
+### Start Test Databases
+
+Before running tests, start the test databases:
+
+```bash
+make test-db-up
+```
+
+This command:
+- Starts PostgreSQL on port 5433
+- Starts MySQL on port 3307
+- Waits for databases to be healthy
+
+### Run All Tests
+
+```bash
+make test
+```
+
+This runs all tests with coverage reporting.
+
+### Run Tests with Automatic Database Management
+
+```bash
+make test-with-db
+```
+
+This command:
+1. Starts test databases
+2. Runs all tests
+3. Stops test databases
+
+Perfect for CI/CD environments!
+
+### Run Tests with Coverage
+
+```bash
+make test-coverage
+```
+
+This generates an HTML coverage report and opens it in your browser.
+
+### Stop Test Databases
+
+```bash
+make test-db-down
+```
+
+### Run Tests for Specific Package
+
+```bash
+# Run all tests in a package
+go test -v ./internal/user/repository
+
+# Run a specific test
+go test -v ./internal/user/repository -run TestPostgreSQLUserRepository_Create
+
+# Run with race detection
+go test -v -race ./internal/user/repository
+```
+
+## 📝 Writing Tests
+
+### Repository Tests
+
+Repository tests use real databases to verify SQL queries and database interactions.
+
+**Test Structure**:
+
+```go
+package repository
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/allisson/go-project-template/internal/testutil"
+ "github.com/allisson/go-project-template/internal/user/domain"
+)
+
+func TestPostgreSQLUserRepository_Create(t *testing.T) {
+ // Setup: Connect to database and run migrations
+ db := testutil.SetupPostgresDB(t)
+ defer testutil.TeardownDB(t, db) // Close connection
+ defer testutil.CleanupPostgresDB(t, db) // Clean up test data
+
+ // Create repository
+ repo := NewPostgreSQLUserRepository(db)
+ ctx := context.Background()
+
+ // Prepare test data
+ user := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: "John Doe",
+ Email: "john@example.com",
+ Password: "hashed_password",
+ }
+
+ // Execute test
+ err := repo.Create(ctx, user)
+
+ // Assert results
+ assert.NoError(t, err)
+
+ // Verify by querying the real database
+ createdUser, err := repo.GetByID(ctx, user.ID)
+ assert.NoError(t, err)
+ assert.Equal(t, user.Name, createdUser.Name)
+ assert.Equal(t, user.Email, createdUser.Email)
+}
+```
+
+**Key Points**:
+- 🔄 Use `defer` for cleanup (connection and data)
+- 🧹 Clean up test data to prevent test pollution
+- ✅ Verify operations by querying the database
+- 🎯 Test one thing per test function
+
+### Testing Error Cases
+
+```go
+func TestPostgreSQLUserRepository_GetByID_NotFound(t *testing.T) {
+ db := testutil.SetupPostgresDB(t)
+ defer testutil.TeardownDB(t, db)
+ defer testutil.CleanupPostgresDB(t, db)
+
+ repo := NewPostgreSQLUserRepository(db)
+ ctx := context.Background()
+
+ // Try to get non-existent user
+ nonExistentID := uuid.Must(uuid.NewV7())
+ user, err := repo.GetByID(ctx, nonExistentID)
+
+ // Verify error handling
+ assert.Error(t, err)
+ assert.Nil(t, user)
+ assert.ErrorIs(t, err, domain.ErrUserNotFound)
+}
+```
+
+### Testing Unique Constraints
+
+```go
+func TestPostgreSQLUserRepository_Create_DuplicateEmail(t *testing.T) {
+ db := testutil.SetupPostgresDB(t)
+ defer testutil.TeardownDB(t, db)
+ defer testutil.CleanupPostgresDB(t, db)
+
+ repo := NewPostgreSQLUserRepository(db)
+ ctx := context.Background()
+
+ // Create first user
+ user1 := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: "John Doe",
+ Email: "john@example.com",
+ Password: "password1",
+ }
+ err := repo.Create(ctx, user1)
+ assert.NoError(t, err)
+
+ // Try to create second user with same email
+ user2 := &domain.User{
+ ID: uuid.Must(uuid.NewV7()),
+ Name: "Jane Doe",
+ Email: "john@example.com", // Duplicate email
+ Password: "password2",
+ }
+ err = repo.Create(ctx, user2)
+
+ // Verify unique constraint error
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, domain.ErrUserAlreadyExists)
+}
+```
+
+### Use Case Tests
+
+Use case tests can use real repositories or mocks depending on the scenario.
+
+**Testing with Real Repository**:
+
+```go
+func TestUserUseCase_RegisterUser(t *testing.T) {
+ db := testutil.SetupPostgresDB(t)
+ defer testutil.TeardownDB(t, db)
+ defer testutil.CleanupPostgresDB(t, db)
+
+ // Setup dependencies
+ userRepo := repository.NewPostgreSQLUserRepository(db)
+ outboxRepo := outboxRepository.NewPostgreSQLOutboxRepository(db)
+ txManager := database.NewTxManager(db)
+ passwordHasher := pwdhash.NewArgon2Hasher(pwdhash.Argon2Config{})
+
+ // Create use case
+ uc := usecase.NewUserUseCase(txManager, userRepo, outboxRepo, passwordHasher)
+ ctx := context.Background()
+
+ // Prepare input
+ input := usecase.RegisterUserInput{
+ Name: "John Doe",
+ Email: "john@example.com",
+ Password: "SecurePass123!",
+ }
+
+ // Execute
+ user, err := uc.RegisterUser(ctx, input)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, input.Name, user.Name)
+ assert.Equal(t, input.Email, user.Email)
+
+ // Verify user was created in database
+ createdUser, err := userRepo.GetByEmail(ctx, input.Email)
+ assert.NoError(t, err)
+ assert.Equal(t, user.ID, createdUser.ID)
+
+ // Verify outbox event was created
+ events, err := outboxRepo.GetPending(ctx, 10)
+ assert.NoError(t, err)
+ assert.Len(t, events, 1)
+ assert.Equal(t, "user.created", events[0].EventType)
+}
+```
+
+### HTTP Handler Tests
+
+HTTP handler tests verify the presentation layer.
+
+```go
+func TestUserHandler_RegisterUser(t *testing.T) {
+ db := testutil.SetupPostgresDB(t)
+ defer testutil.TeardownDB(t, db)
+ defer testutil.CleanupPostgresDB(t, db)
+
+ // Setup dependencies
+ userRepo := repository.NewPostgreSQLUserRepository(db)
+ outboxRepo := outboxRepository.NewPostgreSQLOutboxRepository(db)
+ txManager := database.NewTxManager(db)
+ passwordHasher := pwdhash.NewArgon2Hasher(pwdhash.Argon2Config{})
+ uc := usecase.NewUserUseCase(txManager, userRepo, outboxRepo, passwordHasher)
+ logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
+ handler := http.NewUserHandler(uc, logger)
+
+ // Prepare request
+ reqBody := `{
+ "name": "John Doe",
+ "email": "john@example.com",
+ "password": "SecurePass123!"
+ }`
+ req := httptest.NewRequest("POST", "/api/users", strings.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+
+ // Execute
+ handler.RegisterUser(w, req)
+
+ // Assert response
+ assert.Equal(t, http.StatusCreated, w.Code)
+
+ var response dto.UserResponse
+ err := json.Unmarshal(w.Body.Bytes(), &response)
+ assert.NoError(t, err)
+ assert.Equal(t, "John Doe", response.Name)
+ assert.Equal(t, "john@example.com", response.Email)
+}
+```
+
+### Testing Validation Errors
+
+```go
+func TestUserHandler_RegisterUser_ValidationError(t *testing.T) {
+ // Setup (minimal dependencies for validation test)
+ logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
+ handler := http.NewUserHandler(nil, logger)
+
+ // Invalid request (missing required fields)
+ reqBody := `{
+ "name": "",
+ "email": "invalid-email",
+ "password": "weak"
+ }`
+ req := httptest.NewRequest("POST", "/api/users", strings.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+
+ // Execute
+ handler.RegisterUser(w, req)
+
+ // Assert validation error response
+ assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
+
+ var errorResponse map[string]string
+ err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
+ assert.NoError(t, err)
+ assert.Equal(t, "invalid_input", errorResponse["error"])
+ assert.Contains(t, errorResponse["message"], "name")
+ assert.Contains(t, errorResponse["message"], "email")
+ assert.Contains(t, errorResponse["message"], "password")
+}
+```
+
+## 📊 Test Coverage
+
+### Viewing Coverage Reports
+
+```bash
+# Generate and view HTML coverage report
+make test-coverage
+```
+
+This opens an HTML report in your browser showing:
+- 📈 Overall coverage percentage
+- 📁 Coverage by package
+- 📄 Line-by-line coverage highlighting
+
+### Coverage Goals
+
+Aim for these coverage targets:
+- **Domain Layer**: 90%+ (core business logic)
+- **Use Case Layer**: 85%+ (business orchestration)
+- **Repository Layer**: 90%+ (data access)
+- **HTTP Layer**: 80%+ (handlers and DTOs)
+
+### Checking Coverage from Command Line
+
+```bash
+# Run tests with coverage
+go test -cover ./...
+
+# Generate detailed coverage report
+go test -coverprofile=coverage.out ./...
+go tool cover -func=coverage.out
+
+# View coverage in browser
+go tool cover -html=coverage.out
+```
+
+## 🔍 Test Naming Conventions
+
+Use descriptive test names that follow this pattern:
+
+```
+Test{Type}_{Method}_{Scenario}
+```
+
+**Examples**:
+- `TestPostgreSQLUserRepository_Create`
+- `TestPostgreSQLUserRepository_GetByID_NotFound`
+- `TestUserUseCase_RegisterUser_DuplicateEmail`
+- `TestUserHandler_RegisterUser_ValidationError`
+
+**Benefits**:
+- ✅ Easy to identify what's being tested
+- ✅ Clear understanding of test scenarios
+- ✅ Better test failure messages
+
+## 🏃 CI/CD Testing
+
+### GitHub Actions
+
+The project includes a GitHub Actions workflow (`.github/workflows/ci.yml`) that:
+
+1. ✅ Starts PostgreSQL (port 5433) and MySQL (port 3307) containers
+2. ✅ Waits for both databases to be healthy
+3. ✅ Runs all tests with race detection
+4. ✅ Generates coverage reports
+5. ✅ Uploads coverage to Codecov
+
+**CI Configuration**:
+- Same database credentials as local tests (testuser/testpassword/testdb)
+- Same port mappings as Docker Compose (5433 for Postgres, 3307 for MySQL)
+- Runs on every push to `main` and all pull requests
+- All tests must pass before merging
+
+### Running Tests Like CI Locally
+
+```bash
+# Exact same command as CI
+make test-with-db
+```
+
+This ensures consistency between local development and CI environments.
+
+## 🛠️ Debugging Tests
+
+### Run Tests with Verbose Output
+
+```bash
+go test -v ./internal/user/repository
+```
+
+### Run Single Test
+
+```bash
+go test -v ./internal/user/repository -run TestPostgreSQLUserRepository_Create
+```
+
+### Enable Race Detection
+
+```bash
+go test -race ./...
+```
+
+### Print Test Output
+
+```go
+func TestSomething(t *testing.T) {
+ t.Logf("Debug info: %v", someValue)
+
+ // Or use fmt for immediate output
+ fmt.Printf("Debug info: %v\n", someValue)
+}
+```
+
+### Check Database State During Tests
+
+```go
+func TestUserRepository_Create(t *testing.T) {
+ db := testutil.SetupPostgresDB(t)
+ defer testutil.TeardownDB(t, db)
+ defer testutil.CleanupPostgresDB(t, db)
+
+ repo := NewPostgreSQLUserRepository(db)
+
+ // Create user
+ err := repo.Create(ctx, user)
+ assert.NoError(t, err)
+
+ // Manually query database to verify
+ var count int
+ err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", user.Email).Scan(&count)
+ assert.NoError(t, err)
+ assert.Equal(t, 1, count)
+}
+```
+
+## 🎯 Best Practices
+
+### DO ✅
+
+- ✅ Use real databases for repository tests
+- ✅ Clean up test data after each test
+- ✅ Test both success and error cases
+- ✅ Use descriptive test names
+- ✅ Test one thing per test function
+- ✅ Use table-driven tests for multiple scenarios
+- ✅ Run tests before committing
+- ✅ Maintain high test coverage
+- ✅ Test edge cases and boundary conditions
+
+### DON'T ❌
+
+- ❌ Share test data between tests
+- ❌ Rely on test execution order
+- ❌ Skip cleanup in defer statements
+- ❌ Use production databases for testing
+- ❌ Hardcode database credentials
+- ❌ Leave test databases running
+- ❌ Commit commented-out tests
+- ❌ Test implementation details instead of behavior
+
+## 📚 Testing Resources
+
+- [Go Testing Documentation](https://golang.org/pkg/testing/)
+- [testify/assert](https://github.com/stretchr/testify) - Assertion library
+- [Testing Best Practices](https://golang.org/doc/effective_go#testing)
+- [Table Driven Tests](https://github.com/golang/go/wiki/TableDrivenTests)
diff --git a/internal/outbox/domain/outbox_event.go b/internal/outbox/domain/outbox_event.go
index b07d31e..5cb4cd2 100644
--- a/internal/outbox/domain/outbox_event.go
+++ b/internal/outbox/domain/outbox_event.go
@@ -18,13 +18,13 @@ const (
// OutboxEvent represents an event in the transactional outbox pattern
type OutboxEvent struct {
- ID uuid.UUID `db:"id" json:"id"`
- EventType string `db:"event_type" json:"event_type" fieldtag:"insert,update"`
- Payload string `db:"payload" json:"payload" fieldtag:"insert,update"`
- Status OutboxEventStatus `db:"status" json:"status" fieldtag:"insert,update"`
- Retries int `db:"retries" json:"retries" fieldtag:"insert,update"`
- LastError *string `db:"last_error" json:"last_error,omitempty" fieldtag:"insert,update"`
- ProcessedAt *time.Time `db:"processed_at" json:"processed_at,omitempty" fieldtag:"insert,update"`
- CreatedAt time.Time `db:"created_at" json:"created_at"`
- UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+ ID uuid.UUID
+ EventType string
+ Payload string
+ Status OutboxEventStatus
+ Retries int
+ LastError *string
+ ProcessedAt *time.Time
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
diff --git a/internal/user/domain/user.go b/internal/user/domain/user.go
index 8ff5eae..ce32dde 100644
--- a/internal/user/domain/user.go
+++ b/internal/user/domain/user.go
@@ -11,12 +11,12 @@ import (
// User represents a user in the system
type User struct {
- ID uuid.UUID `db:"id"`
- Name string `db:"name" fieldtag:"insert,update"`
- Email string `db:"email" fieldtag:"insert,update"`
- Password string `db:"password" fieldtag:"insert,update"`
- CreatedAt time.Time `db:"created_at"`
- UpdatedAt time.Time `db:"updated_at"`
+ ID uuid.UUID
+ Name string
+ Email string
+ Password string
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
// Domain-specific errors for user operations.