API / old /refactoring-architecture.md
sshinmen's picture
Update HF Spaces deployment with latest changes
2e6b65c
|
Raw
History Blame Contribute Delete
10.2 kB

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:

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

  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