moicle 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,76 @@
1
- # Go Backend Structure
1
+ # Go Backend Architecture
2
2
 
3
- > Simple Handler + Service pattern with GORM
3
+ > Production-grade Clean Architecture with Modular Design
4
4
 
5
5
  ## Project Structure
6
6
 
7
7
  ```
8
8
  {project}/
9
9
  ├── cmd/
10
- └── api/
11
- └── main.go # Entry point
10
+ ├── api/
11
+ │ ├── main.go # Entry point (HTTP + gRPC + SSE)
12
+ │ │ └── router.go # Route definitions & middleware
13
+ │ └── background/
14
+ │ ├── main.go # CLI entry (Cobra)
15
+ │ ├── commands/ # CLI subcommands
16
+ │ │ ├── consumer.go # Task consumer
17
+ │ │ ├── scheduler.go # Task scheduler
18
+ │ │ └── sync.go # Sync command
19
+ │ └── consumers/ # Task handlers (Asynq)
20
+ │ ├── notification.go
21
+ │ └── cleanup.go
12
22
  ├── internal/
13
23
  │ ├── config/
14
- │ │ └── config.go # App configuration
15
- │ ├── models/ # GORM models
16
- │ │ ├── user.go
17
- │ │ └── story.go
18
- │ ├── handlers/ # HTTP handlers
19
- │ │ ├── user_handler.go
20
- │ │ └── story_handler.go
21
- ├── services/ # Business logic
22
- ├── user_service.go
23
- │ │ └── story_service.go
24
- ├── middleware/ # HTTP middleware
25
- ├── auth.go
26
- └── cors.go
27
- │ └── routes/ # Route definitions
28
- └── routes.go
29
- ├── pkg/ # Shared packages
30
- │ ├── database/ # DB connection
31
- ├── logger/
32
- │ └── validator/
33
- ├── migrations/ # SQL migrations
34
- ├── .claude/
35
- ├── CLAUDE.md
24
+ │ │ └── config.go # App configuration
25
+ │ ├── middleware/
26
+ │ │ ├── auth.go # Auth middleware + caching
27
+ │ │ ├── admin_auth.go # Admin auth
28
+ ├── cors.go # CORS
29
+ │ │ ├── logger.go # Request logging
30
+ │ │ ├── recovery.go # Panic recovery
31
+ │ └── api_key.go # API key validation
32
+ │ ├── grpc/ # gRPC services (optional)
33
+ │ │ └── account_service.go
34
+ └── modules/ # Feature modules
35
+ ├── auth/
36
+ ├── controllers/
37
+ └── auth_controller.go
38
+ │ ├── usecases/
39
+ │ │ │ └── auth_usecase.go
40
+ ├── dtos/
41
+ │ └── auth_dto.go
42
+ └── init.go # Module init & routes
43
+ ├── user/
44
+ │ │ ├── controllers/
45
+ │ │ ├── usecases/
46
+ │ │ ├── dtos/
47
+ │ │ └── init.go
48
+ │ └── {feature}/ # Other modules...
49
+ │ ├── controllers/
50
+ │ ├── usecases/
51
+ │ ├── dtos/
52
+ │ ├── validators/ # (optional) Chain validators
53
+ │ └── init.go
54
+ ├── pkg/
55
+ │ ├── database/
56
+ │ │ ├── database.go # GORM connection
57
+ │ │ ├── user.go # User model
58
+ │ │ └── {entity}.go # Other models
59
+ │ ├── enums/ # Enumeration types
60
+ │ ├── events/ # Event definitions
61
+ │ ├── queue/ # Task queue (Asynq)
62
+ │ │ ├── tasks.go # Task definitions
63
+ │ │ └── client.go # Queue client
64
+ │ ├── redis/ # Redis client
65
+ │ ├── sse/ # Server-Sent Events
66
+ │ │ └── hub.go # SSE Hub
67
+ │ ├── response/ # HTTP response helpers
68
+ │ ├── logger/ # Structured logging
69
+ │ ├── storage/ # File storage (S3/R2)
70
+ │ └── utils/ # Utility functions
71
+ ├── migrations/ # SQL migrations
72
+ ├── chart/ # Kubernetes Helm charts (optional)
73
+ ├── .env.example
36
74
  ├── go.mod
37
75
  ├── Makefile
38
76
  ├── Dockerfile
@@ -42,136 +80,893 @@
42
80
  ## Architecture Pattern
43
81
 
44
82
  ```
45
- HandlerServiceModel (GORM)
46
-
47
- Request Database
48
- Binding Query
83
+ ControllerUsecaseDatabase (GORM)
84
+
85
+ Request Business
86
+ Binding Logic
87
+ ↓ ↓
88
+ DTOs Models
49
89
  ```
50
90
 
51
- **Simple flow:**
52
- 1. Handler receives HTTP request
53
- 2. Handler calls Service
54
- 3. Service contains business logic
55
- 4. Service uses GORM Models directly
56
- 5. Handler returns response
91
+ **Layer Responsibilities:**
92
+
93
+ | Layer | Responsibility |
94
+ |-------|----------------|
95
+ | Controller | HTTP handling, request/response, validation |
96
+ | Usecase | Business logic, orchestration |
97
+ | DTO | Data transfer objects, API contracts |
98
+ | Model | Database entities (pkg/database) |
99
+
100
+ ## Module Structure
101
+
102
+ Each feature is an independent module:
103
+
104
+ ```
105
+ internal/modules/{feature}/
106
+ ├── controllers/
107
+ │ └── {feature}_controller.go # HTTP handlers
108
+ ├── usecases/
109
+ │ └── {feature}_usecase.go # Business logic
110
+ ├── dtos/
111
+ │ └── {feature}_dto.go # Request/Response DTOs
112
+ ├── validators/ # (optional)
113
+ │ └── {feature}_validator.go # Chain validators
114
+ └── init.go # Module init & route registration
115
+ ```
57
116
 
58
117
  ## Key Files
59
118
 
60
- ### internal/models/user.go
119
+ ### cmd/api/main.go
61
120
  ```go
62
- package models
121
+ package main
63
122
 
64
- import "gorm.io/gorm"
123
+ import (
124
+ "context"
125
+ "log"
126
+ "net/http"
127
+ "os"
128
+ "os/signal"
129
+ "syscall"
130
+ "time"
65
131
 
66
- type User struct {
67
- gorm.Model
68
- Name string `json:"name"`
69
- Email string `json:"email" gorm:"uniqueIndex"`
70
- Password string `json:"-"`
132
+ "myapp/internal/config"
133
+ "myapp/pkg/database"
134
+ "myapp/pkg/redis"
135
+ "myapp/pkg/queue"
136
+ "myapp/pkg/sse"
137
+ )
138
+
139
+ func main() {
140
+ cfg := config.Load()
141
+
142
+ // Initialize dependencies
143
+ db := database.Connect(cfg.Database)
144
+ redisClient := redis.NewClient(cfg.Redis)
145
+ queueClient := queue.NewClient(redisClient)
146
+ sseHub := sse.NewHub(redisClient)
147
+
148
+ // Start SSE Hub
149
+ go sseHub.Run()
150
+
151
+ // Setup router
152
+ router := SetupRouter(db, redisClient, queueClient, sseHub, cfg)
153
+
154
+ // HTTP Server
155
+ server := &http.Server{
156
+ Addr: ":" + cfg.Port,
157
+ Handler: router,
158
+ ReadTimeout: 15 * time.Second,
159
+ WriteTimeout: 0, // Disabled for SSE
160
+ }
161
+
162
+ // Graceful shutdown
163
+ go func() {
164
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
165
+ log.Fatalf("Server error: %v", err)
166
+ }
167
+ }()
168
+
169
+ quit := make(chan os.Signal, 1)
170
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
171
+ <-quit
172
+
173
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
174
+ defer cancel()
175
+
176
+ if err := server.Shutdown(ctx); err != nil {
177
+ log.Fatalf("Shutdown error: %v", err)
178
+ }
179
+ }
180
+ ```
181
+
182
+ ### cmd/api/router.go
183
+ ```go
184
+ package main
185
+
186
+ import (
187
+ "github.com/gin-gonic/gin"
188
+ "gorm.io/gorm"
189
+
190
+ "myapp/internal/config"
191
+ "myapp/internal/middleware"
192
+ "myapp/internal/modules/auth"
193
+ "myapp/internal/modules/user"
194
+ "myapp/pkg/queue"
195
+ "myapp/pkg/redis"
196
+ "myapp/pkg/sse"
197
+ )
198
+
199
+ func SetupRouter(
200
+ db *gorm.DB,
201
+ redisClient *redis.Client,
202
+ queueClient *queue.Client,
203
+ sseHub *sse.Hub,
204
+ cfg *config.Config,
205
+ ) *gin.Engine {
206
+ r := gin.New()
207
+
208
+ // Global middleware
209
+ r.Use(middleware.Recovery())
210
+ r.Use(middleware.Logger())
211
+ r.Use(middleware.CORS(cfg.AllowedOrigins))
212
+
213
+ // Health check
214
+ r.GET("/health", func(c *gin.Context) {
215
+ c.JSON(200, gin.H{"status": "ok"})
216
+ })
217
+
218
+ // Auth middleware
219
+ authMiddleware := middleware.NewAuthMiddleware(db, redisClient, cfg.Firebase)
220
+
221
+ // Initialize modules
222
+ auth.Init(r, db, redisClient, authMiddleware)
223
+ user.Init(r, db, authMiddleware)
224
+
225
+ return r
71
226
  }
72
227
  ```
73
228
 
74
- ### internal/services/user_service.go
229
+ ### internal/modules/auth/init.go
75
230
  ```go
76
- package services
231
+ package auth
77
232
 
78
233
  import (
79
- "myapp/internal/models"
234
+ "github.com/gin-gonic/gin"
80
235
  "gorm.io/gorm"
236
+
237
+ "myapp/internal/middleware"
238
+ "myapp/internal/modules/auth/controllers"
239
+ "myapp/internal/modules/auth/usecases"
240
+ "myapp/pkg/redis"
241
+ )
242
+
243
+ func Init(
244
+ r *gin.Engine,
245
+ db *gorm.DB,
246
+ redisClient *redis.Client,
247
+ authMiddleware *middleware.AuthMiddleware,
248
+ ) {
249
+ // Initialize usecase
250
+ usecase := usecases.NewAuthUsecase(db, redisClient)
251
+
252
+ // Initialize controller
253
+ controller := controllers.NewAuthController(usecase)
254
+
255
+ // Register routes
256
+ authGroup := r.Group("/auth")
257
+ {
258
+ authGroup.POST("/verify", controller.VerifyToken)
259
+
260
+ // Protected routes
261
+ protected := authGroup.Group("")
262
+ protected.Use(authMiddleware.Authenticate())
263
+ {
264
+ protected.GET("/me", controller.GetMe)
265
+ protected.PUT("/me", controller.UpdateMe)
266
+ }
267
+ }
268
+ }
269
+ ```
270
+
271
+ ### internal/modules/auth/controllers/auth_controller.go
272
+ ```go
273
+ package controllers
274
+
275
+ import (
276
+ "net/http"
277
+
278
+ "github.com/gin-gonic/gin"
279
+
280
+ "myapp/internal/modules/auth/dtos"
281
+ "myapp/internal/modules/auth/usecases"
282
+ "myapp/pkg/response"
81
283
  )
82
284
 
83
- type UserService struct {
84
- db *gorm.DB
285
+ type AuthController struct {
286
+ usecase *usecases.AuthUsecase
85
287
  }
86
288
 
87
- func NewUserService(db *gorm.DB) *UserService {
88
- return &UserService{db: db}
289
+ func NewAuthController(usecase *usecases.AuthUsecase) *AuthController {
290
+ return &AuthController{usecase: usecase}
89
291
  }
90
292
 
91
- func (s *UserService) GetByID(id uint) (*models.User, error) {
92
- var user models.User
93
- if err := s.db.First(&user, id).Error; err != nil {
293
+ func (c *AuthController) VerifyToken(ctx *gin.Context) {
294
+ var req dtos.VerifyTokenRequest
295
+ if err := ctx.ShouldBindJSON(&req); err != nil {
296
+ response.Error(ctx, http.StatusBadRequest, "Invalid request")
297
+ return
298
+ }
299
+
300
+ user, err := c.usecase.VerifyToken(ctx, req.Token)
301
+ if err != nil {
302
+ response.Error(ctx, http.StatusUnauthorized, "Invalid token")
303
+ return
304
+ }
305
+
306
+ response.Success(ctx, dtos.ToUserResponse(user))
307
+ }
308
+
309
+ func (c *AuthController) GetMe(ctx *gin.Context) {
310
+ userID := ctx.GetString("user_id")
311
+
312
+ user, err := c.usecase.GetByID(ctx, userID)
313
+ if err != nil {
314
+ response.Error(ctx, http.StatusNotFound, "User not found")
315
+ return
316
+ }
317
+
318
+ response.Success(ctx, dtos.ToUserResponse(user))
319
+ }
320
+
321
+ func (c *AuthController) UpdateMe(ctx *gin.Context) {
322
+ userID := ctx.GetString("user_id")
323
+
324
+ var req dtos.UpdateUserRequest
325
+ if err := ctx.ShouldBindJSON(&req); err != nil {
326
+ response.Error(ctx, http.StatusBadRequest, "Invalid request")
327
+ return
328
+ }
329
+
330
+ user, err := c.usecase.Update(ctx, userID, &req)
331
+ if err != nil {
332
+ response.Error(ctx, http.StatusInternalServerError, err.Error())
333
+ return
334
+ }
335
+
336
+ response.Success(ctx, dtos.ToUserResponse(user))
337
+ }
338
+ ```
339
+
340
+ ### internal/modules/auth/usecases/auth_usecase.go
341
+ ```go
342
+ package usecases
343
+
344
+ import (
345
+ "context"
346
+ "errors"
347
+
348
+ "gorm.io/gorm"
349
+
350
+ "myapp/internal/modules/auth/dtos"
351
+ "myapp/pkg/database"
352
+ "myapp/pkg/firebase"
353
+ "myapp/pkg/redis"
354
+ )
355
+
356
+ type AuthUsecase struct {
357
+ db *gorm.DB
358
+ redisClient *redis.Client
359
+ }
360
+
361
+ func NewAuthUsecase(db *gorm.DB, redisClient *redis.Client) *AuthUsecase {
362
+ return &AuthUsecase{
363
+ db: db,
364
+ redisClient: redisClient,
365
+ }
366
+ }
367
+
368
+ func (u *AuthUsecase) VerifyToken(ctx context.Context, token string) (*database.User, error) {
369
+ // Verify Firebase token
370
+ firebaseToken, err := firebase.VerifyToken(ctx, token)
371
+ if err != nil {
94
372
  return nil, err
95
373
  }
374
+
375
+ // Find or create user
376
+ var user database.User
377
+ result := u.db.Where("firebase_uid = ?", firebaseToken.UID).First(&user)
378
+
379
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
380
+ // Create new user
381
+ user = database.User{
382
+ FirebaseUID: firebaseToken.UID,
383
+ Email: firebaseToken.Claims["email"].(string),
384
+ }
385
+ if err := u.db.Create(&user).Error; err != nil {
386
+ return nil, err
387
+ }
388
+ } else if result.Error != nil {
389
+ return nil, result.Error
390
+ }
391
+
96
392
  return &user, nil
97
393
  }
98
394
 
99
- func (s *UserService) GetAll() ([]models.User, error) {
100
- var users []models.User
101
- if err := s.db.Find(&users).Error; err != nil {
395
+ func (u *AuthUsecase) GetByID(ctx context.Context, id string) (*database.User, error) {
396
+ var user database.User
397
+ if err := u.db.First(&user, "id = ?", id).Error; err != nil {
102
398
  return nil, err
103
399
  }
104
- return users, nil
400
+ return &user, nil
105
401
  }
106
402
 
107
- func (s *UserService) Create(user *models.User) error {
108
- return s.db.Create(user).Error
403
+ func (u *AuthUsecase) Update(ctx context.Context, id string, req *dtos.UpdateUserRequest) (*database.User, error) {
404
+ var user database.User
405
+ if err := u.db.First(&user, "id = ?", id).Error; err != nil {
406
+ return nil, err
407
+ }
408
+
409
+ updates := map[string]interface{}{}
410
+ if req.Name != nil {
411
+ updates["name"] = *req.Name
412
+ }
413
+ if req.Phone != nil {
414
+ updates["phone"] = *req.Phone
415
+ }
416
+
417
+ if err := u.db.Model(&user).Updates(updates).Error; err != nil {
418
+ return nil, err
419
+ }
420
+
421
+ return &user, nil
422
+ }
423
+ ```
424
+
425
+ ### internal/modules/auth/dtos/auth_dto.go
426
+ ```go
427
+ package dtos
428
+
429
+ import "myapp/pkg/database"
430
+
431
+ type VerifyTokenRequest struct {
432
+ Token string `json:"token" binding:"required"`
433
+ }
434
+
435
+ type UpdateUserRequest struct {
436
+ Name *string `json:"name"`
437
+ Phone *string `json:"phone"`
438
+ }
439
+
440
+ type UserResponse struct {
441
+ ID string `json:"id"`
442
+ Email string `json:"email"`
443
+ Name *string `json:"name"`
444
+ Phone *string `json:"phone"`
445
+ AvatarURL *string `json:"avatar_url"`
446
+ CreatedAt string `json:"created_at"`
447
+ }
448
+
449
+ func ToUserResponse(user *database.User) *UserResponse {
450
+ return &UserResponse{
451
+ ID: user.ID,
452
+ Email: user.Email,
453
+ Name: user.Name,
454
+ Phone: user.Phone,
455
+ AvatarURL: user.AvatarURL,
456
+ CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"),
457
+ }
109
458
  }
110
459
  ```
111
460
 
112
- ### internal/handlers/user_handler.go
461
+ ### internal/middleware/auth.go
113
462
  ```go
114
- package handlers
463
+ package middleware
115
464
 
116
465
  import (
117
466
  "net/http"
118
- "myapp/internal/services"
467
+ "strings"
468
+ "time"
469
+
119
470
  "github.com/gin-gonic/gin"
471
+ "gorm.io/gorm"
472
+
473
+ "myapp/pkg/database"
474
+ "myapp/pkg/firebase"
475
+ "myapp/pkg/redis"
120
476
  )
121
477
 
122
- type UserHandler struct {
123
- userService *services.UserService
478
+ type AuthMiddleware struct {
479
+ db *gorm.DB
480
+ redisClient *redis.Client
481
+ }
482
+
483
+ func NewAuthMiddleware(db *gorm.DB, redisClient *redis.Client) *AuthMiddleware {
484
+ return &AuthMiddleware{
485
+ db: db,
486
+ redisClient: redisClient,
487
+ }
488
+ }
489
+
490
+ func (m *AuthMiddleware) Authenticate() gin.HandlerFunc {
491
+ return func(c *gin.Context) {
492
+ authHeader := c.GetHeader("Authorization")
493
+ if authHeader == "" {
494
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization header"})
495
+ return
496
+ }
497
+
498
+ token := strings.TrimPrefix(authHeader, "Bearer ")
499
+
500
+ // Check Redis cache first
501
+ cacheKey := "auth:" + token[:32]
502
+ if cached, err := m.redisClient.Get(c, cacheKey); err == nil {
503
+ c.Set("user_id", cached)
504
+ c.Next()
505
+ return
506
+ }
507
+
508
+ // Verify Firebase token
509
+ firebaseToken, err := firebase.VerifyToken(c, token)
510
+ if err != nil {
511
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
512
+ return
513
+ }
514
+
515
+ // Get user from database
516
+ var user database.User
517
+ if err := m.db.Where("firebase_uid = ?", firebaseToken.UID).First(&user).Error; err != nil {
518
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
519
+ return
520
+ }
521
+
522
+ // Cache for 1 hour
523
+ m.redisClient.Set(c, cacheKey, user.ID, time.Hour)
524
+
525
+ c.Set("user_id", user.ID)
526
+ c.Set("user", &user)
527
+ c.Next()
528
+ }
529
+ }
530
+
531
+ func (m *AuthMiddleware) Optional() gin.HandlerFunc {
532
+ return func(c *gin.Context) {
533
+ authHeader := c.GetHeader("Authorization")
534
+ if authHeader == "" {
535
+ c.Next()
536
+ return
537
+ }
538
+
539
+ token := strings.TrimPrefix(authHeader, "Bearer ")
540
+
541
+ firebaseToken, err := firebase.VerifyToken(c, token)
542
+ if err != nil {
543
+ c.Next()
544
+ return
545
+ }
546
+
547
+ var user database.User
548
+ if err := m.db.Where("firebase_uid = ?", firebaseToken.UID).First(&user).Error; err == nil {
549
+ c.Set("user_id", user.ID)
550
+ c.Set("user", &user)
551
+ }
552
+
553
+ c.Next()
554
+ }
124
555
  }
556
+ ```
557
+
558
+ ### pkg/database/database.go
559
+ ```go
560
+ package database
561
+
562
+ import (
563
+ "fmt"
564
+ "log"
125
565
 
126
- func NewUserHandler(userService *services.UserService) *UserHandler {
127
- return &UserHandler{userService: userService}
566
+ "gorm.io/driver/mysql"
567
+ "gorm.io/gorm"
568
+ "gorm.io/gorm/logger"
569
+ )
570
+
571
+ type Config struct {
572
+ Host string
573
+ Port string
574
+ User string
575
+ Password string
576
+ Name string
128
577
  }
129
578
 
130
- func (h *UserHandler) GetAll(c *gin.Context) {
131
- users, err := h.userService.GetAll()
579
+ func Connect(cfg Config) *gorm.DB {
580
+ dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=UTC",
581
+ cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)
582
+
583
+ db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
584
+ Logger: logger.Default.LogMode(logger.Info),
585
+ })
132
586
  if err != nil {
133
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
134
- return
587
+ log.Fatalf("Failed to connect to database: %v", err)
588
+ }
589
+
590
+ sqlDB, _ := db.DB()
591
+ sqlDB.SetMaxOpenConns(100)
592
+ sqlDB.SetMaxIdleConns(10)
593
+
594
+ // Auto-migrate models
595
+ db.AutoMigrate(
596
+ &User{},
597
+ // Add other models here
598
+ )
599
+
600
+ return db
601
+ }
602
+ ```
603
+
604
+ ### pkg/database/user.go
605
+ ```go
606
+ package database
607
+
608
+ import (
609
+ "time"
610
+
611
+ "github.com/google/uuid"
612
+ "gorm.io/gorm"
613
+ )
614
+
615
+ type User struct {
616
+ ID string `gorm:"type:char(36);primaryKey" json:"id"`
617
+ FirebaseUID string `gorm:"type:varchar(128);uniqueIndex" json:"-"`
618
+ Email string `gorm:"type:varchar(255);uniqueIndex" json:"email"`
619
+ Name *string `gorm:"type:varchar(255)" json:"name"`
620
+ Phone *string `gorm:"type:varchar(20)" json:"phone"`
621
+ AvatarURL *string `gorm:"type:varchar(500)" json:"avatar_url"`
622
+ Status string `gorm:"type:varchar(20);default:active" json:"status"`
623
+ CreatedAt time.Time `json:"created_at"`
624
+ UpdatedAt time.Time `json:"updated_at"`
625
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
626
+ }
627
+
628
+ func (u *User) BeforeCreate(tx *gorm.DB) error {
629
+ if u.ID == "" {
630
+ u.ID = uuid.New().String()
135
631
  }
136
- c.JSON(http.StatusOK, gin.H{"data": users})
632
+ return nil
633
+ }
634
+ ```
635
+
636
+ ### pkg/response/response.go
637
+ ```go
638
+ package response
639
+
640
+ import "github.com/gin-gonic/gin"
641
+
642
+ type Response struct {
643
+ Success bool `json:"success"`
644
+ Data interface{} `json:"data,omitempty"`
645
+ Error string `json:"error,omitempty"`
646
+ Meta *Meta `json:"meta,omitempty"`
137
647
  }
138
648
 
139
- func (h *UserHandler) GetByID(c *gin.Context) {
140
- id := c.Param("id")
141
- user, err := h.userService.GetByID(id)
649
+ type Meta struct {
650
+ Page int `json:"page"`
651
+ PerPage int `json:"per_page"`
652
+ Total int64 `json:"total"`
653
+ TotalPages int `json:"total_pages"`
654
+ }
655
+
656
+ func Success(c *gin.Context, data interface{}) {
657
+ c.JSON(200, Response{
658
+ Success: true,
659
+ Data: data,
660
+ })
661
+ }
662
+
663
+ func SuccessWithMeta(c *gin.Context, data interface{}, meta *Meta) {
664
+ c.JSON(200, Response{
665
+ Success: true,
666
+ Data: data,
667
+ Meta: meta,
668
+ })
669
+ }
670
+
671
+ func Error(c *gin.Context, status int, message string) {
672
+ c.JSON(status, Response{
673
+ Success: false,
674
+ Error: message,
675
+ })
676
+ }
677
+
678
+ func Created(c *gin.Context, data interface{}) {
679
+ c.JSON(201, Response{
680
+ Success: true,
681
+ Data: data,
682
+ })
683
+ }
684
+ ```
685
+
686
+ ### pkg/queue/tasks.go (Asynq)
687
+ ```go
688
+ package queue
689
+
690
+ import (
691
+ "encoding/json"
692
+
693
+ "github.com/hibiken/asynq"
694
+ )
695
+
696
+ const (
697
+ TaskSendNotification = "notification:send"
698
+ TaskSyncData = "sync:data"
699
+ TaskCleanup = "cleanup:expired"
700
+ )
701
+
702
+ type NotificationPayload struct {
703
+ UserID string `json:"user_id"`
704
+ Title string `json:"title"`
705
+ Body string `json:"body"`
706
+ Data map[string]string `json:"data"`
707
+ }
708
+
709
+ func NewSendNotificationTask(payload *NotificationPayload) (*asynq.Task, error) {
710
+ data, err := json.Marshal(payload)
142
711
  if err != nil {
143
- c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
144
- return
712
+ return nil, err
145
713
  }
146
- c.JSON(http.StatusOK, gin.H{"data": user})
714
+ return asynq.NewTask(TaskSendNotification, data), nil
147
715
  }
148
716
  ```
149
717
 
150
- ### cmd/api/main.go
718
+ ### pkg/sse/hub.go (Server-Sent Events)
719
+ ```go
720
+ package sse
721
+
722
+ import (
723
+ "context"
724
+ "encoding/json"
725
+ "sync"
726
+
727
+ "myapp/pkg/redis"
728
+ )
729
+
730
+ type Client struct {
731
+ UserID string
732
+ Channel chan []byte
733
+ }
734
+
735
+ type Hub struct {
736
+ clients map[string]map[*Client]bool
737
+ register chan *Client
738
+ unregister chan *Client
739
+ broadcast chan *Message
740
+ mutex sync.RWMutex
741
+ redisClient *redis.Client
742
+ }
743
+
744
+ type Message struct {
745
+ UserID string `json:"user_id,omitempty"`
746
+ Type string `json:"type"`
747
+ Data interface{} `json:"data"`
748
+ }
749
+
750
+ func NewHub(redisClient *redis.Client) *Hub {
751
+ return &Hub{
752
+ clients: make(map[string]map[*Client]bool),
753
+ register: make(chan *Client),
754
+ unregister: make(chan *Client),
755
+ broadcast: make(chan *Message, 256),
756
+ redisClient: redisClient,
757
+ }
758
+ }
759
+
760
+ func (h *Hub) Run() {
761
+ // Subscribe to Redis for cross-instance communication
762
+ go h.subscribeRedis()
763
+
764
+ for {
765
+ select {
766
+ case client := <-h.register:
767
+ h.mutex.Lock()
768
+ if h.clients[client.UserID] == nil {
769
+ h.clients[client.UserID] = make(map[*Client]bool)
770
+ }
771
+ h.clients[client.UserID][client] = true
772
+ h.mutex.Unlock()
773
+
774
+ case client := <-h.unregister:
775
+ h.mutex.Lock()
776
+ if clients, ok := h.clients[client.UserID]; ok {
777
+ delete(clients, client)
778
+ close(client.Channel)
779
+ if len(clients) == 0 {
780
+ delete(h.clients, client.UserID)
781
+ }
782
+ }
783
+ h.mutex.Unlock()
784
+
785
+ case message := <-h.broadcast:
786
+ h.broadcastLocal(message)
787
+ }
788
+ }
789
+ }
790
+
791
+ func (h *Hub) BroadcastUser(userID string, msgType string, data interface{}) {
792
+ msg := &Message{
793
+ UserID: userID,
794
+ Type: msgType,
795
+ Data: data,
796
+ }
797
+
798
+ // Publish to Redis for cross-instance
799
+ payload, _ := json.Marshal(msg)
800
+ h.redisClient.Publish(context.Background(), "sse:broadcast", payload)
801
+ }
802
+
803
+ func (h *Hub) broadcastLocal(msg *Message) {
804
+ h.mutex.RLock()
805
+ defer h.mutex.RUnlock()
806
+
807
+ payload, _ := json.Marshal(msg)
808
+
809
+ if msg.UserID != "" {
810
+ if clients, ok := h.clients[msg.UserID]; ok {
811
+ for client := range clients {
812
+ select {
813
+ case client.Channel <- payload:
814
+ default:
815
+ close(client.Channel)
816
+ delete(clients, client)
817
+ }
818
+ }
819
+ }
820
+ }
821
+ }
822
+
823
+ func (h *Hub) subscribeRedis() {
824
+ pubsub := h.redisClient.Subscribe(context.Background(), "sse:broadcast")
825
+ ch := pubsub.Channel()
826
+
827
+ for msg := range ch {
828
+ var message Message
829
+ if err := json.Unmarshal([]byte(msg.Payload), &message); err == nil {
830
+ h.broadcast <- &message
831
+ }
832
+ }
833
+ }
834
+
835
+ func (h *Hub) Register(client *Client) {
836
+ h.register <- client
837
+ }
838
+
839
+ func (h *Hub) Unregister(client *Client) {
840
+ h.unregister <- client
841
+ }
842
+ ```
843
+
844
+ ### cmd/background/main.go (Cobra CLI)
151
845
  ```go
152
846
  package main
153
847
 
154
848
  import (
849
+ "log"
850
+
851
+ "github.com/spf13/cobra"
852
+
853
+ "myapp/cmd/background/commands"
854
+ )
855
+
856
+ func main() {
857
+ rootCmd := &cobra.Command{
858
+ Use: "worker",
859
+ Short: "Background worker CLI",
860
+ }
861
+
862
+ rootCmd.AddCommand(commands.ConsumerCmd())
863
+ rootCmd.AddCommand(commands.SchedulerCmd())
864
+ rootCmd.AddCommand(commands.SyncCmd())
865
+
866
+ if err := rootCmd.Execute(); err != nil {
867
+ log.Fatal(err)
868
+ }
869
+ }
870
+ ```
871
+
872
+ ### cmd/background/commands/consumer.go
873
+ ```go
874
+ package commands
875
+
876
+ import (
877
+ "log"
878
+
879
+ "github.com/hibiken/asynq"
880
+ "github.com/spf13/cobra"
881
+
882
+ "myapp/cmd/background/consumers"
155
883
  "myapp/internal/config"
156
- "myapp/internal/handlers"
157
- "myapp/internal/services"
158
884
  "myapp/pkg/database"
885
+ "myapp/pkg/queue"
159
886
  )
160
887
 
161
- func main() {
162
- cfg := config.Load()
163
- db := database.Connect(cfg.DB)
888
+ func ConsumerCmd() *cobra.Command {
889
+ return &cobra.Command{
890
+ Use: "consumer",
891
+ Short: "Start task consumer",
892
+ Run: func(cmd *cobra.Command, args []string) {
893
+ cfg := config.Load()
894
+ db := database.Connect(cfg.Database)
164
895
 
165
- // Wire dependencies
166
- userService := services.NewUserService(db)
167
- userHandler := handlers.NewUserHandler(userService)
896
+ srv := asynq.NewServer(
897
+ asynq.RedisClientOpt{Addr: cfg.Redis.Addr},
898
+ asynq.Config{
899
+ Concurrency: 10,
900
+ },
901
+ )
168
902
 
169
- // Setup routes
170
- r := gin.Default()
171
- r.GET("/users", userHandler.GetAll)
172
- r.GET("/users/:id", userHandler.GetByID)
903
+ mux := asynq.NewServeMux()
904
+ mux.HandleFunc(queue.TaskSendNotification, consumers.HandleSendNotification(db))
905
+ mux.HandleFunc(queue.TaskCleanup, consumers.HandleCleanup(db))
173
906
 
174
- r.Run(cfg.Port)
907
+ if err := srv.Run(mux); err != nil {
908
+ log.Fatalf("Failed to run server: %v", err)
909
+ }
910
+ },
911
+ }
912
+ }
913
+ ```
914
+
915
+ ## Validator Chain Pattern (Optional)
916
+
917
+ For complex validations:
918
+
919
+ ```go
920
+ package validators
921
+
922
+ import (
923
+ "context"
924
+ "errors"
925
+ )
926
+
927
+ type Validator interface {
928
+ Validate(ctx context.Context, data interface{}) error
929
+ }
930
+
931
+ type ValidatorChain struct {
932
+ validators []Validator
933
+ }
934
+
935
+ func NewValidatorChain() *ValidatorChain {
936
+ return &ValidatorChain{
937
+ validators: make([]Validator, 0),
938
+ }
939
+ }
940
+
941
+ func (c *ValidatorChain) Add(v Validator) *ValidatorChain {
942
+ c.validators = append(c.validators, v)
943
+ return c
944
+ }
945
+
946
+ func (c *ValidatorChain) Validate(ctx context.Context, data interface{}) error {
947
+ for _, v := range c.validators {
948
+ if err := v.Validate(ctx, data); err != nil {
949
+ return err
950
+ }
951
+ }
952
+ return nil
953
+ }
954
+
955
+ // Example validator
956
+ type AmountValidator struct {
957
+ MinAmount int64
958
+ MaxAmount int64
959
+ }
960
+
961
+ func (v *AmountValidator) Validate(ctx context.Context, data interface{}) error {
962
+ amount := data.(int64)
963
+ if amount < v.MinAmount {
964
+ return errors.New("amount too small")
965
+ }
966
+ if amount > v.MaxAmount {
967
+ return errors.New("amount too large")
968
+ }
969
+ return nil
175
970
  }
176
971
  ```
177
972
 
@@ -179,39 +974,66 @@ func main() {
179
974
 
180
975
  | Item | Convention | Example |
181
976
  |------|------------|---------|
182
- | Package | lowercase, short | `user`, `auth` |
183
- | Struct | PascalCase | `UserService` |
184
- | File | snake_case | `user_handler.go` |
185
- | Handler func | PascalCase | `GetByID`, `Create` |
186
- | Service func | PascalCase | `GetAll`, `FindByEmail` |
977
+ | Package | lowercase, short | `auth`, `user` |
978
+ | Module folder | snake_case | `bank_account/` |
979
+ | File | snake_case | `auth_controller.go` |
980
+ | Struct | PascalCase | `AuthController` |
981
+ | Interface | PascalCase | `Validator` |
982
+ | Function | PascalCase | `GetByID`, `Create` |
983
+ | Private func | camelCase | `validateToken` |
984
+ | Constants | PascalCase | `TaskSendNotification` |
985
+ | Enums | PascalCase | `StatusActive` |
187
986
 
188
987
  ## Makefile
189
988
 
190
989
  ```makefile
191
- .PHONY: run build test
990
+ .PHONY: dev run build test migrate
991
+
992
+ dev:
993
+ air
192
994
 
193
995
  run:
194
996
  go run cmd/api/main.go
195
997
 
196
998
  build:
197
999
  go build -o bin/api cmd/api/main.go
1000
+ go build -o bin/worker cmd/background/main.go
198
1001
 
199
1002
  test:
200
1003
  go test -v ./...
201
1004
 
202
1005
  migrate:
203
1006
  go run cmd/migrate/main.go
1007
+
1008
+ worker-consumer:
1009
+ go run cmd/background/main.go consumer
1010
+
1011
+ worker-scheduler:
1012
+ go run cmd/background/main.go scheduler
204
1013
  ```
205
1014
 
206
- ## When to Add More Structure
1015
+ ## Tech Stack
1016
+
1017
+ | Component | Technology |
1018
+ |-----------|------------|
1019
+ | Framework | Gin |
1020
+ | ORM | GORM |
1021
+ | Database | MySQL/PostgreSQL |
1022
+ | Cache | Redis |
1023
+ | Queue | Asynq |
1024
+ | Auth | Firebase Auth |
1025
+ | CLI | Cobra |
1026
+ | gRPC | google.golang.org/grpc |
1027
+ | Storage | AWS S3 / Cloudflare R2 |
207
1028
 
208
- **Current pattern is enough for:**
209
- - Small to medium APIs
210
- - CRUD operations
211
- - Simple business logic
1029
+ ## When to Use What
212
1030
 
213
- **Consider adding layers when:**
214
- - Multiple data sources (DB + external APIs)
215
- - Complex business rules
216
- - Need to swap database
217
- - Large team with clear boundaries
1031
+ | Scenario | Solution |
1032
+ |----------|----------|
1033
+ | Simple CRUD | Controller → Usecase → Model |
1034
+ | Complex validation | Validator Chain pattern |
1035
+ | Background jobs | Asynq task queue |
1036
+ | Real-time events | SSE Hub + Redis pub/sub |
1037
+ | Inter-service | gRPC |
1038
+ | File upload | Storage package (S3/R2) |
1039
+ | Push notifications | FCM via queue |