Clean Architecture Refactoring Plan
Current State Analysis
API Handlers (internal/api/handlers/management/)
The current handlers have several issues:
- Tight coupling: Handlers directly manipulate config, auth, and logging
- Mixed concerns: Business logic mixed with HTTP transport (Gin)
- Inconsistent error handling: Uses
gin.H{"error": ...}directly - No clear separation: Domain logic embedded in HTTP handlers
- Large files:
auth_files.gois ~2200 lines with multiple responsibilities
Auth Module (internal/auth/)
- Minimal interface: Only
TokenStorageinterface defined - Provider-specific logic: Each provider has its own auth implementation
- No unified error types: Each provider handles errors differently
Logging Module (internal/logging/)
- Good interfaces:
RequestLoggerandStreamingLogWriteralready defined - Mixed transport concerns: Some Gin-specific code in logging
- Correlation IDs: Basic request ID support exists but could be enhanced
Proposed Clean Architecture
Layer Structure
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Transport Layer β
β (Gin handlers, middleware, routing) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Application Layer β
β (Use cases, DTOs, request/response mapping) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Domain Layer β
β (Domain services, entities, business rules, interfaces) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Infrastructure Layer β
β (Config persistence, auth storage, file system, HTTP) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
New Directory Structure
internal/
βββ api/
β βββ handlers/ # HTTP transport (thin layer)
β β βββ management/
β β βββ handler.go
β β βββ config_handler.go
β β βββ auth_handler.go
β β βββ logs_handler.go
β βββ middleware/ # HTTP middleware
β βββ auth.go
β βββ logging.go
β βββ error_handler.go
βββ application/ # Application layer (NEW)
β βββ dto/ # Data transfer objects
β β βββ config_dto.go
β β βββ auth_dto.go
β β βββ response_dto.go
β βββ mapper/ # DTO <-> Domain mapping
β β βββ config_mapper.go
β β βββ auth_mapper.go
β βββ usecase/ # Use cases
β βββ config_usecase.go
β βββ auth_usecase.go
β βββ logs_usecase.go
βββ domain/ # Domain layer (NEW)
β βββ entity/ # Domain entities
β β βββ config.go
β β βββ auth.go
β β βββ log_entry.go
β βββ service/ # Domain services (business logic)
β β βββ config_service.go
β β βββ auth_service.go
β β βββ log_service.go
β βββ repository/ # Repository interfaces
β β βββ config_repository.go
β β βββ auth_repository.go
β βββ error/ # Domain errors
β βββ errors.go
βββ infrastructure/ # Infrastructure layer (NEW)
β βββ persistence/ # Repository implementations
β β βββ config_repository.go
β β βββ auth_repository.go
β βββ auth/ # Auth provider implementations
β β βββ provider.go
β β βββ factory.go
β βββ logging/ # Logging infrastructure
β βββ file_logger.go
β βββ structured_logger.go
βββ interfaces/ # Existing - shared interfaces
βββ types.go
Key Components
1. Domain Errors (internal/domain/error/errors.go)
Standardized error types for the entire application:
package error
type DomainError struct {
Code string
Message string
Cause error
}
func (e *DomainError) Error() string { ... }
// Specific error types
var (
ErrNotFound = &DomainError{Code: "NOT_FOUND", ...}
ErrUnauthorized = &DomainError{Code: "UNAUTHORIZED", ...}
ErrInvalidInput = &DomainError{Code: "INVALID_INPUT", ...}
ErrInternal = &DomainError{Code: "INTERNAL_ERROR", ...}
)
2. Domain Services
Domain services contain pure business logic, no HTTP concerns:
// internal/domain/service/config_service.go
type ConfigService interface {
GetConfig(ctx context.Context) (*entity.Config, error)
UpdateConfig(ctx context.Context, cfg *entity.Config) error
UpdateField(ctx context.Context, field string, value any) error
ValidateConfig(ctx context.Context, cfg *entity.Config) error
}
// internal/domain/service/auth_service.go
type AuthService interface {
ListAuthFiles(ctx context.Context) ([]*entity.AuthFile, error)
UploadAuthFile(ctx context.Context, file *entity.AuthFile) error
DeleteAuthFile(ctx context.Context, id string) error
RefreshToken(ctx context.Context, id string) (*entity.AuthToken, error)
}
3. Repository Interfaces
// internal/domain/repository/config_repository.go
type ConfigRepository interface {
Load(ctx context.Context) (*entity.Config, error)
Save(ctx context.Context, cfg *entity.Config) error
Validate(ctx context.Context, cfg *entity.Config) error
}
// internal/domain/repository/auth_repository.go
type AuthRepository interface {
List(ctx context.Context) ([]*entity.AuthFile, error)
GetByID(ctx context.Context, id string) (*entity.AuthFile, error)
Save(ctx context.Context, file *entity.AuthFile) error
Delete(ctx context.Context, id string) error
}
4. Application Use Cases
Use cases orchestrate domain services for specific operations:
// internal/application/usecase/config_usecase.go
type ConfigUseCase struct {
configService domain.ConfigService
logger logging.Logger
}
func (uc *ConfigUseCase) GetConfig(ctx context.Context) (*dto.ConfigResponse, error) {
cfg, err := uc.configService.GetConfig(ctx)
if err != nil {
uc.logger.Error("failed to get config", err)
return nil, err
}
return mapper.ToConfigResponse(cfg), nil
}
5. Structured Logging with Correlation IDs
// internal/infrastructure/logging/structured_logger.go
type StructuredLogger struct {
logger *logrus.Logger
}
type LogEntry struct {
Timestamp time.Time
Level string
Message string
CorrelationID string
Service string
Operation string
Fields map[string]interface{}
}
func (l *StructuredLogger) WithCorrelationID(ctx context.Context) *logrus.Entry {
correlationID := logging.GetRequestID(ctx)
return l.logger.WithField("correlation_id", correlationID)
}
6. HTTP Handlers (Thin Layer)
Handlers only deal with HTTP concerns:
// internal/api/handlers/management/config_handler.go
type ConfigHandler struct {
useCase *usecase.ConfigUseCase
}
func (h *ConfigHandler) GetConfig(c *gin.Context) {
ctx := c.Request.Context()
response, err := h.useCase.GetConfig(ctx)
if err != nil {
h.handleError(c, err)
return
}
c.JSON(http.StatusOK, response)
}
func (h *ConfigHandler) handleError(c *gin.Context, err error) {
// Map domain errors to HTTP responses
var domainErr *domainerror.DomainError
if errors.As(err, &domainErr) {
status := h.mapErrorCodeToStatus(domainErr.Code)
c.JSON(status, gin.H{
"error": domainErr.Code,
"message": domainErr.Message,
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
})
}
Migration Strategy
Phase 1: Foundation
- Create domain error types
- Define repository interfaces
- Create domain service interfaces
- Set up structured logging infrastructure
Phase 2: Extract Domain Logic
- Move business logic from handlers to domain services
- Implement repository interfaces
- Create use cases
- Add comprehensive unit tests
Phase 3: Refactor Handlers
- Make handlers thin - delegate to use cases
- Standardize error handling
- Add correlation ID middleware
Phase 4: Cleanup
- Remove old code
- Verify backward compatibility
- Update documentation
Backward Compatibility
- All existing API endpoints remain unchanged
- Response formats preserved
- Configuration file format unchanged
- Auth file format unchanged
Testing Strategy
- Unit tests: Domain services with mocked repositories
- Integration tests: Use cases with real repositories
- E2E tests: Full HTTP request/response cycle
- Mock implementations: For all external dependencies
Benefits
- Testability: Domain logic can be tested without HTTP layer
- Maintainability: Clear separation of concerns
- Flexibility: Easy to swap implementations (e.g., different storage)
- Observability: Structured logging with correlation IDs
- Scalability: Clear boundaries for future extensions