| # Clean Architecture Refactoring Plan |
|
|
| ## Current State Analysis |
|
|
| ### API Handlers (`internal/api/handlers/management/`) |
| The current handlers have several issues: |
| 1. **Tight coupling**: Handlers directly manipulate config, auth, and logging |
| 2. **Mixed concerns**: Business logic mixed with HTTP transport (Gin) |
| 3. **Inconsistent error handling**: Uses `gin.H{"error": ...}` directly |
| 4. **No clear separation**: Domain logic embedded in HTTP handlers |
| 5. **Large files**: `auth_files.go` is ~2200 lines with multiple responsibilities |
|
|
| ### Auth Module (`internal/auth/`) |
| 1. **Minimal interface**: Only `TokenStorage` interface defined |
| 2. **Provider-specific logic**: Each provider has its own auth implementation |
| 3. **No unified error types**: Each provider handles errors differently |
|
|
| ### Logging Module (`internal/logging/`) |
| 1. **Good interfaces**: `RequestLogger` and `StreamingLogWriter` already defined |
| 2. **Mixed transport concerns**: Some Gin-specific code in logging |
| 3. **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: |
|
|
| ```go |
| 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: |
|
|
| ```go |
| // 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 |
|
|
| ```go |
| // 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: |
|
|
| ```go |
| // 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 |
|
|
| ```go |
| // 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: |
|
|
| ```go |
| // 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 |
| 1. Create domain error types |
| 2. Define repository interfaces |
| 3. Create domain service interfaces |
| 4. Set up structured logging infrastructure |
|
|
| ### Phase 2: Extract Domain Logic |
| 1. Move business logic from handlers to domain services |
| 2. Implement repository interfaces |
| 3. Create use cases |
| 4. Add comprehensive unit tests |
|
|
| ### Phase 3: Refactor Handlers |
| 1. Make handlers thin - delegate to use cases |
| 2. Standardize error handling |
| 3. Add correlation ID middleware |
|
|
| ### Phase 4: Cleanup |
| 1. Remove old code |
| 2. Verify backward compatibility |
| 3. Update documentation |
|
|
| ## Backward Compatibility |
|
|
| - All existing API endpoints remain unchanged |
| - Response formats preserved |
| - Configuration file format unchanged |
| - Auth file format unchanged |
|
|
| ## Testing Strategy |
|
|
| 1. **Unit tests**: Domain services with mocked repositories |
| 2. **Integration tests**: Use cases with real repositories |
| 3. **E2E tests**: Full HTTP request/response cycle |
| 4. **Mock implementations**: For all external dependencies |
|
|
| ## Benefits |
|
|
| 1. **Testability**: Domain logic can be tested without HTTP layer |
| 2. **Maintainability**: Clear separation of concerns |
| 3. **Flexibility**: Easy to swap implementations (e.g., different storage) |
| 4. **Observability**: Structured logging with correlation IDs |
| 5. **Scalability**: Clear boundaries for future extensions |
|
|