Architecture Overview

The B12 SIS backend follows Clean Architecture principles with a layered design that separates concerns and promotes testability.

High-Level Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         HTTP Layer                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │   Gin    │  │  Routes  │  │Middleware│  │   Controllers    │ │
│  │  Router  │─▶│ Setup    │─▶│  Chain   │─▶│   (Handlers)     │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘ │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                       Business Layer                             │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                      Services                             │   │
│  │  • Business logic                                         │   │
│  │  • Validation                                             │   │
│  │  • Authorization                                          │   │
│  │  • Cross-cutting concerns                                 │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                        Data Layer                                │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    Repositories                           │   │
│  │  • CRUD operations                                        │   │
│  │  • Query building                                         │   │
│  │  • Transaction management                                 │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                      GORM + MySQL                         │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Directory Structure

b12-backend/
├── cmd/
│   └── api/
│       └── main.go           # Application entry point
│
├── internal/
│   ├── controllers/          # HTTP handlers (87 files)
│   │   ├── base_controller.go    # Generic CRUD base controller
│   │   ├── auth_controller.go    # Authentication endpoints
│   │   ├── admin_controller.go   # System administration
│   │   ├── student_controller.go
│   │   └── ... (70+ entity controllers)
│   │
│   ├── services/             # Business logic (104 files)
│   │   ├── base_service.go       # Generic CRUD base service
│   │   ├── auth_service.go       # Authentication logic
│   │   ├── auth_cache_service.go # User data caching
│   │   ├── gradebook_calculation_service.go
│   │   └── ... (entity services)
│   │
│   ├── repositories/         # Data access (86 files)
│   │   ├── base_repository.go    # Generic CRUD base repository
│   │   └── ... (entity repositories)
│   │
│   ├── models/               # Domain entities (87 files)
│   │   ├── base.go               # BaseModel with common fields
│   │   ├── users.go              # User accounts
│   │   ├── student.go            # Student profiles
│   │   ├── teachers.go           # Teacher profiles
│   │   └── ... (80+ domain models)
│   │
│   ├── middlewares/          # HTTP middleware (5 files)
│   │   ├── auth.go               # JWT validation & user loading
│   │   ├── audit.go              # Security event tracking
│   │   ├── api_log.go            # API request logging
│   │   ├── cors.go               # CORS configuration
│   │   └── language.go           # i18n language selection
│   │
│   ├── routes/               # Route registration
│   │   └── routes.go             # 1551 lines, 70+ entity routes
│   │
│   ├── jobs/                 # Background jobs (57 files, 114 jobs)
│   │   ├── registry.go           # Job registration factory
│   │   ├── init.go               # Job initialization
│   │   ├── import_processor_job.go
│   │   ├── canvas_sync_*.go      # Canvas LMS sync jobs
│   │   └── ... (gradebook, report, sync jobs)
│   │
│   ├── hooks/                # External integrations
│   │   └── canvas_hooks.go       # Canvas LMS hooks
│   │
│   ├── utils/                # Shared utilities (43 files)
│   │   ├── filter.go             # Filter parsing
│   │   ├── response.go           # HTTP response helpers
│   │   ├── validation.go         # Input validation
│   │   ├── context.go            # Context utilities
│   │   ├── jwt.go                # JWT token handling
│   │   ├── i18n.go               # Internationalization
│   │   ├── canvas_integration.go # Canvas API client
│   │   ├── s3.go                 # AWS S3 file upload
│   │   └── ...
│   │
│   ├── interfaces/           # Shared interfaces (6 files)
│   │   ├── person.go             # Person fields interface
│   │   ├── address.go            # Address fields interfaces
│   │   ├── canvas_lms.go         # Canvas LMS fields
│   │   ├── mobile_app.go         # Mobile app fields
│   │   ├── searchable.go         # Search interface
│   │   └── team_filter.go        # Team filtering interface
│   │
│   └── types/                # Custom types
│       └── date.go               # Custom date type
│
├── locales/                  # i18n translations
│   ├── en.json
│   └── vi.json
│
├── docs/                     # Documentation
│
└── helm/                     # Kubernetes charts

Layer Responsibilities

Controllers (HTTP Layer)

Controllers handle HTTP requests and responses:

  • Parse request parameters and body

  • Validate input structure

  • Call appropriate service methods

  • Format and return responses

  • Handle HTTP-specific errors

func (c *StudentController) GetByID(ctx *gin.Context) {
    id := ctx.Param("id")
    student, err := c.service.GetByID(utils.GetContext(ctx), id)
    if err != nil {
        utils.ErrorResponse(ctx, http.StatusNotFound, "Student not found", err.Error())
        return
    }
    utils.SuccessResponse(ctx, "Student retrieved", student)
}

Services (Business Layer)

Services contain business logic:

  • Business rule validation

  • Authorization checks

  • Orchestrating repository calls

  • Cross-cutting concerns (logging, audit)

  • External integrations

func (s *StudentService) Create(ctx context.Context, input StudentInput) (*Student, error) {
    // Business validation
    if err := s.validateStudent(input); err != nil {
        return nil, err
    }

    // Generate student code
    code, err := s.autoCodeService.GenerateCode("Student")
    if err != nil {
        return nil, err
    }

    student := mapInputToStudent(input)
    student.Code = code

    return s.repository.Create(ctx, student)
}

Repositories (Data Layer)

Repositories handle data persistence:

  • CRUD operations

  • Query building with filters

  • Transaction management

  • Preloading relationships

func (r *StudentRepository) FindByCode(ctx context.Context, code string) (*Student, error) {
    var student Student
    err := r.DB.WithContext(ctx).
        Where("code = ?", code).
        Preload("Class").
        First(&student).Error
    return &student, err
}

Generic CRUD Pattern

The system uses Go generics for common CRUD operations:

BaseRepository[T] → BaseService[T, CreateInput, UpdateInput] → BaseController[T, CreateInput, UpdateInput]

This reduces boilerplate and ensures consistency. See Generic CRUD Pattern for details.

Middleware Chain

Requests flow through middleware in order:

Request → CORS → Auth → Team → AcademicSession → DataChange → Controller

Middleware

Purpose

CORS

Handle cross-origin requests

Auth

Validate JWT and extract user

Team

Validate and set team context

AcademicSession

Set academic session context

DataChange

Audit data modifications

Permission

Check user permissions

Database Design

Soft Deletes

All models use soft delete via deleted_at timestamp:

type BaseModel struct {
    Id        string          `gorm:"type:char(36);primary_key"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt  `gorm:"index"`
}

Multi-tenancy

Data isolation via team_id:

type BaseModel struct {
    // ...
    TeamId    string `gorm:"size:36;index"`
}

Audit Fields

Every record tracks who created/modified it:

type BaseModel struct {
    // ...
    CreatedById string `gorm:"size:36;index"`
    UpdatedById string `gorm:"size:36;index"`
    DeletedById string `gorm:"size:36;index"`
}

External Integrations

Canvas LMS

Integration via hooks pattern:

// Model AfterSave hook
func (s *Student) AfterSave(tx *gorm.DB) error {
    go func() {
        if canvasHooks := hooks.GetCanvasHooks(); canvasHooks != nil {
            canvasHooks.SyncStudentIfNeeded(ctx, studentData, isUpdate)
        }
    }()
    return nil
}

Background Jobs

Jobs implement the Job interface:

type Job interface {
    Name() string
    Schedule() string  // Cron expression
    Execute() error
}

Configuration

Configuration via environment variables with .env file support. See Configuration Guide.

Error Handling

Consistent error handling pattern:

// Service layer - return errors
if err := validate(input); err != nil {
    return nil, fmt.Errorf("validation failed: %w", err)
}

// Controller layer - format response
if err != nil {
    utils.ErrorResponse(ctx, http.StatusBadRequest, "Invalid input", err.Error())
    return
}

Testing Strategy

  • Unit Tests: Service and repository logic

  • Integration Tests: API endpoints with test database

  • Table-driven Tests: Go idiom for multiple test cases

func TestStudentService_Create(t *testing.T) {
    tests := []struct {
        name    string
        input   StudentInput
        wantErr bool
    }{
        {"valid student", validInput, false},
        {"missing name", missingNameInput, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := service.Create(ctx, tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}