This document guides the refactoring to remove global state and add context.Context throughout the codebase.
✅ Created internal/database/database.go - Database service wrapper
✅ Created internal/logging/logger.go - Logger service wrapper
✅ Reference Implementation: User/Auth domain fully refactored
Before:
type UserRepository interface {
GetByID(id uint) (*models.User, error)
}
func (r *userRepository) GetByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
return &user, err
}After:
type UserRepository interface {
GetByID(ctx context.Context, id uint) (*models.User, error)
}
func (r *userRepository) GetByID(ctx context.Context, id uint) (*models.User, error) {
var user models.User
err := r.db.WithContext(ctx).First(&user, id).Error
return &user, err
}Before:
type UserService interface {
GetByID(id uint) (*models.User, error)
}
func (s *userService) GetByID(id uint) (*models.User, error) {
return s.userRepo.GetByID(id)
}After:
type UserService interface {
GetByID(ctx context.Context, id uint) (*models.User, error)
}
func (s *userService) GetByID(ctx context.Context, id uint) (*models.User, error) {
return s.userRepo.GetByID(ctx, id)
}Before:
func (h *UserHandler) Get(c *gin.Context) {
id := parseID(c)
user, err := h.userService.GetByID(id)
// ...
}After:
func (h *UserHandler) Get(c *gin.Context) {
ctx := c.Request.Context()
id := parseID(c)
user, err := h.userService.GetByID(ctx, id)
// ...
}Before:
utils.Logger.Info("message")
logger := utils.LogServiceCall(utils.Logger, "Service", "Method")After:
// Inject logger into services/repositories
type userService struct {
logger *logging.Logger
userRepo repository.UserRepository
}
func (s *userService) GetByID(ctx context.Context, id uint) (*models.User, error) {
s.logger.WithContext(ctx).WithField("user_id", id).Debug("Getting user")
return s.userRepo.GetByID(ctx, id)
}Each domain follows the same pattern. Refactor in this order:
- internal/repository/user_repository.go
- internal/service/user_service.go
- internal/service/auth_service.go
- internal/handler/user_handler.go
- internal/handler/auth_handler.go
- internal/repository/lead_repository.go
- internal/service/lead_service.go
- internal/handler/lead_handler.go
- internal/repository/customer_repository.go
- internal/service/customer_service.go
- internal/handler/customer_handler.go
- internal/repository/ticket_repository.go
- internal/service/ticket_service.go
- internal/handler/ticket_handler.go
- internal/repository/task_repository.go
- internal/service/task_service.go
- internal/handler/task_handler.go
- internal/repository/apikey_repository.go
- internal/service/apikey_service.go
- internal/handler/apikey_handler.go
- internal/repository/configuration_repository.go
- internal/service/configuration_service.go
- internal/handler/configuration_handler.go
- internal/service/dashboard_service.go (no repository)
- internal/handler/dashboard_handler.go
// In repository implementation
func (r *userRepository) CreateTx(ctx context.Context, tx *gorm.DB, user *models.User) error {
return tx.WithContext(ctx).Create(user).Error
}func (s *leadService) ConvertToCustomer(ctx context.Context, leadID uint, customerData *models.Customer) error {
return s.db.Transaction(ctx, func(tx *gorm.DB) error {
lead, err := s.leadRepo.GetByIDTx(ctx, tx, leadID)
if err != nil {
return err
}
if err := s.customerRepo.CreateTx(ctx, tx, customerData); err != nil {
return err
}
if err := s.leadRepo.ConvertToCustomerTx(ctx, tx, leadID, customerData.ID); err != nil {
return err
}
return nil
})
}// Add preload parameter to list operations
func (r *userRepository) List(ctx context.Context, offset, limit int, preload []string) ([]models.User, error) {
query := r.db.WithContext(ctx)
for _, rel := range preload {
query = query.Preload(rel)
}
var users []models.User
err := query.Offset(offset).Limit(limit).Find(&users).Error
return users, err
}Before:
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)
user, err := service.GetByID(1)After:
mockRepo := new(MockUserRepository)
mockLogger := &logging.Logger{}
service := NewUserService(mockRepo, mockLogger)
ctx := context.Background()
user, err := service.GetByID(ctx, 1)For each domain, follow these steps:
-
Update Repository Interface (internal/repository/interfaces.go)
- Add
ctx context.Contextas first parameter to all methods - Add transaction variants:
CreateTx,UpdateTx, etc. - Add preload options to list methods
- Add
-
Update Repository Implementation
- Add context parameter
- Use
r.db.WithContext(ctx)for all queries - Implement transaction variants
- Add GORM preloading
-
Update Service Interface (internal/service/interfaces.go)
- Add
ctx context.Contextas first parameter
- Add
-
Update Service Implementation
- Add
logger *logging.Loggerto struct - Add context parameter
- Pass context to repository calls
- Use logger.WithContext(ctx) for logging
- Use database.Transaction() for multi-step operations
- Add
-
Update Handler
- Extract
ctx := c.Request.Context() - Pass context to service calls
- Extract
-
Update Tests
- Add
ctx := context.Background() - Pass context to all calls
- Update mocks to expect context
- Add
- Request Cancellation: Long-running operations can be cancelled
- Timeout Enforcement: Per-request timeouts can be set
- Distributed Tracing: Request IDs and traces propagate through the stack
- No Global State: Better testability and concurrency safety
- Transaction Safety: Multi-step operations are atomic
- Query Optimization: N+1 queries prevented with preloading
- Per domain (Repository + Service + Handler): ~30-45 minutes
- Tests per domain: ~15-20 minutes
- Total for all 8 domains: ~6-8 hours
- Use the User/Auth reference implementation as a template
- Refactor one domain at a time
- Run tests after each domain
- Commit incrementally
See the completed User/Auth implementation:
internal/repository/user_repository_v2.gointernal/service/user_service_v2.gointernal/service/auth_service_v2.gointernal/handler/user_handler_v2.gointernal/handler/auth_handler_v2.go