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)
}
})
}
}