Guide for implementing Clean Architecture patterns in the Go backend. Use this when adding new features, creating handlers, services, or repositories.
The backend follows Clean Architecture with 4 distinct layers:
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
internal/domain/review.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
}
internal/repository/postgres/review_repo.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...
internal/api/dto/review_dto.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"`
}
internal/service/review_service.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
}
internal/api/handlers/review_handler.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)
}
internal/api/routes.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)
}
}
cmd/api/main.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)
}
pkg/response// 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
}
// 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
}
// Use pkg/response helpers
response.Success(c, http.StatusOK, "Success", data)
response.Error(c, http.StatusBadRequest, "Error message", err)
// Create in internal/api/middleware/
// Apply in routes.go
// Use test database
// Test SQL queries
// Mock database if needed
// Mock repository interfaces
// Test business logic
// Verify error handling
// Use httptest
// Mock services
// Test HTTP responses
internal/domain/internal/repository/postgres/internal/api/dto/internal/service/internal/api/handlers/internal/api/routes.gocmd/api/main.go