Go Clean Architecture Implementation
Architecture Layers
The backend follows Clean Architecture with 4 distinct layers:
code
internal/ ├── api/ # HTTP Layer (handlers, middleware, DTOs) ├── service/ # Business Logic Layer ├── repository/postgres/ # Data Access Layer └── domain/ # Domain Entities & Interfaces
Dependency Flow: Handler → Service → Repository → Database Rule: Inner layers never depend on outer layers
Adding a New Feature (Step-by-Step)
Example: Adding Product Reviews
1. Create Domain Entity (internal/domain/review.go)
go
package domain
import "time"
type Review struct {
ID int `json:"id"`
UserID int `json:"user_id"`
ProductID int `json:"product_id"`
Rating int `json:"rating"`
Comment string `json:"comment"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Repository interface (depends on domain, not implementation)
type ReviewRepository interface {
Create(review *Review) error
GetByID(id int) (*Review, error)
GetByProductID(productID int, limit, offset int) ([]*Review, error)
Update(review *Review) error
Delete(id int) error
}
2. Create Repository Implementation (internal/repository/postgres/review_repo.go)
go
package postgres
import (
"database/sql"
"demo-project/ecommerce-backend/internal/domain"
)
type reviewRepository struct {
db *sql.DB
}
func NewReviewRepository(db *sql.DB) domain.ReviewRepository {
return &reviewRepository{db: db}
}
func (r *reviewRepository) Create(review *domain.Review) error {
query := `
INSERT INTO reviews (user_id, product_id, rating, comment)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at, updated_at
`
return r.db.QueryRow(
query,
review.UserID,
review.ProductID,
review.Rating,
review.Comment,
).Scan(&review.ID, &review.CreatedAt, &review.UpdatedAt)
}
// Implement other methods...
3. Create DTOs (internal/api/dto/review_dto.go)
go
package dto
type CreateReviewRequest struct {
ProductID int `json:"product_id" validate:"required,min=1"`
Rating int `json:"rating" validate:"required,min=1,max=5"`
Comment string `json:"comment" validate:"max=1000"`
}
type ReviewResponse struct {
ID int `json:"id"`
UserID int `json:"user_id"`
ProductID int `json:"product_id"`
Rating int `json:"rating"`
Comment string `json:"comment"`
UserName string `json:"user_name,omitempty"`
CreatedAt string `json:"created_at"`
}
4. Create Service (internal/service/review_service.go)
go
package service
import (
"demo-project/ecommerce-backend/internal/domain"
"demo-project/ecommerce-backend/internal/api/dto"
)
type ReviewService struct {
reviewRepo domain.ReviewRepository
userRepo domain.UserRepository
productRepo domain.ProductRepository
}
func NewReviewService(
reviewRepo domain.ReviewRepository,
userRepo domain.UserRepository,
productRepo domain.ProductRepository,
) *ReviewService {
return &ReviewService{
reviewRepo: reviewRepo,
userRepo: userRepo,
productRepo: productRepo,
}
}
func (s *ReviewService) CreateReview(userID int, req *dto.CreateReviewRequest) (*domain.Review, error) {
// 1. Validate product exists
_, err := s.productRepo.GetByID(req.ProductID)
if err != nil {
return nil, domain.ErrNotFound
}
// 2. Check user hasn't already reviewed
// ... business logic ...
// 3. Create review
review := &domain.Review{
UserID: userID,
ProductID: req.ProductID,
Rating: req.Rating,
Comment: req.Comment,
}
if err := s.reviewRepo.Create(review); err != nil {
return nil, err
}
return review, nil
}
5. Create Handler (internal/api/handlers/review_handler.go)
go
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"demo-project/ecommerce-backend/internal/api/dto"
"demo-project/ecommerce-backend/internal/service"
"demo-project/ecommerce-backend/pkg/response"
)
type ReviewHandler struct {
reviewService *service.ReviewService
}
func NewReviewHandler(reviewService *service.ReviewService) *ReviewHandler {
return &ReviewHandler{
reviewService: reviewService,
}
}
// @Summary Create a review
// @Tags reviews
// @Accept json
// @Produce json
// @Param review body dto.CreateReviewRequest true "Review data"
// @Success 201 {object} dto.ReviewResponse
// @Router /reviews [post]
// @Security BearerAuth
func (h *ReviewHandler) CreateReview(c *gin.Context) {
var req dto.CreateReviewRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Invalid request", err)
return
}
// Get user ID from auth middleware
userID := c.GetInt("user_id")
review, err := h.reviewService.CreateReview(userID, &req)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Failed to create review", err)
return
}
response.Success(c, http.StatusCreated, "Review created", review)
}
6. Register Routes (internal/api/routes.go)
go
func SetupRoutes(router *gin.Engine, handlers *Handlers) {
api := router.Group("/api/v1")
// ... existing routes ...
// Review routes
reviews := api.Group("/reviews")
reviews.Use(middleware.AuthMiddleware())
{
reviews.POST("", handlers.ReviewHandler.CreateReview)
reviews.GET("/product/:id", handlers.ReviewHandler.GetProductReviews)
}
}
7. Wire Dependencies (cmd/api/main.go)
go
func main() {
// ... existing setup ...
// Repositories
userRepo := postgres.NewUserRepository(db)
reviewRepo := postgres.NewReviewRepository(db) // Add this
// Services
authService := service.NewAuthService(userRepo)
reviewService := service.NewReviewService(reviewRepo, userRepo, productRepo) // Add this
// Handlers
authHandler := handlers.NewAuthHandler(authService)
reviewHandler := handlers.NewReviewHandler(reviewService) // Add this
handlers := &api.Handlers{
AuthHandler: authHandler,
ReviewHandler: reviewHandler, // Add this
}
// Setup routes
routes.SetupRoutes(router, handlers)
}
Key Principles
1. Repository Layer
- •Only handles database operations
- •Returns domain entities, not DTOs
- •Uses SQL queries or ORM
- •Implements interfaces defined in domain
- •No business logic
2. Service Layer
- •Contains all business logic
- •Orchestrates between repositories
- •Performs validation
- •Handles transactions
- •Returns domain entities
3. Handler Layer
- •Handles HTTP concerns
- •Validates request DTOs
- •Calls service methods
- •Formats responses using
pkg/response - •Handles errors appropriately
4. Domain Layer
- •Pure domain entities
- •Interface definitions
- •Business errors
- •No dependencies on other layers
Common Patterns
Error Handling
go
// Define domain errors in internal/domain/errors.go
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
// In service
if user == nil {
return nil, domain.ErrNotFound
}
// In handler
if errors.Is(err, domain.ErrNotFound) {
response.Error(c, http.StatusNotFound, "Not found", err)
return
}
Validation
go
// Use validator tags in DTOs
type CreateRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
// Validate in handler
if err := validator.Validate(req); err != nil {
response.Error(c, http.StatusBadRequest, "Validation failed", err)
return
}
Standardized Responses
go
// Use pkg/response helpers response.Success(c, http.StatusOK, "Success", data) response.Error(c, http.StatusBadRequest, "Error message", err)
Middleware
go
// Create in internal/api/middleware/ // Apply in routes.go
Testing
Repository Tests
go
// Use test database // Test SQL queries // Mock database if needed
Service Tests
go
// Mock repository interfaces // Test business logic // Verify error handling
Handler Tests
go
// Use httptest // Mock services // Test HTTP responses
Checklist for New Features
- • Create domain entity in
internal/domain/ - • Define repository interface in domain
- • Implement repository in
internal/repository/postgres/ - • Create DTOs in
internal/api/dto/ - • Implement service in
internal/service/ - • Create handler in
internal/api/handlers/ - • Register routes in
internal/api/routes.go - • Wire dependencies in
cmd/api/main.go - • Create database migration
- • Add Swagger comments
- • Write tests
- • Update API documentation