moicle 1.3.1 → 1.4.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,14 +1,16 @@
1
- # CLAUDE.md - Go + Gin Backend Template
1
+ # CLAUDE.md - Go + Gin Backend (DDD Architecture)
2
2
 
3
3
  ## Project Overview
4
4
 
5
5
  Backend API service built with:
6
- - **Go 1.22+** - Programming language
6
+ - **Go 1.24+** - Programming language
7
7
  - **Gin** - HTTP web framework
8
8
  - **GORM** - ORM for database operations
9
9
  - **Redis** - Caching and session storage
10
10
  - **Viper** - Configuration management
11
11
 
12
+ Architecture: **Domain-Driven Design** with strict layer separation. Domain layer has zero infrastructure dependencies.
13
+
12
14
  ## Quick Start
13
15
 
14
16
  ```bash
@@ -16,10 +18,10 @@ Backend API service built with:
16
18
  go mod download
17
19
 
18
20
  # Run development server
19
- go run cmd/api/main.go
21
+ go run cmd/v1/main.go
20
22
 
21
23
  # Build for production
22
- go build -o bin/api cmd/api/main.go
24
+ go build -o bin/api cmd/v1/main.go
23
25
 
24
26
  # Run tests
25
27
  go test ./...
@@ -29,168 +31,610 @@ go test ./...
29
31
 
30
32
  ```
31
33
  {project_name}/
32
- ├── cmd/
33
- └── api/
34
- └── main.go # Application entry point
34
+ ├── cmd/v1/
35
+ ├── main.go # Entry point + AutoMigrate
36
+ └── router.go # Route registration
35
37
  ├── internal/
36
- │ ├── config/ # Configuration loading (Viper)
37
- │ │ └── config.go
38
- ├── middleware/ # HTTP middlewares
39
- │ │ ├── auth.go
40
- │ │ ├── cors.go
41
- │ │ └── logger.go
42
- │ └── modules/ # Feature modules
43
- │ ├── router/
44
- │ │ └── router.go # Route registration
45
- └── {module}/
46
- ├── controllers/
47
- └── controller.go
48
- ├── models/
49
- └── entity.go
50
- ├── usecases/
51
- └── usecase.go
52
- └── init.go
53
- ├── pkg/ # Shared packages
54
- │ ├── database/
55
- │ │ └── database.go
56
- │ ├── queue/
57
- │ │ └── redis.go
58
- │ └── response/
59
- └── response.go
60
- ├── config.yaml # Configuration file
38
+ │ ├── domain/
39
+ │ │ ├── shared/ # Shared types (EventCollector, BaseEvent, EventDispatcher)
40
+ │ └── {domain}/
41
+ │ │ ├── entities/ # Aggregates with behavior + EventCollector
42
+ │ │ └── {entity}.go
43
+ │ │ ├── valueobjects/ # Typed values, only stdlib
44
+ │ │ └── {vo}.go
45
+ ├── ports/ # 1 interface per file
46
+ │ └── {store_name}.go
47
+ ├── events/ # 1 event per file, embed BaseEvent
48
+ │ │ └── {event_name}.go
49
+ ├── usecases/ # Business logic, NO infra imports
50
+ │ │ └── {action}.go
51
+ └── validators/ # (optional) Pure validation
52
+ ├── application/
53
+ ├── ports/http/
54
+ │ │ ├── {module}_handler.go # Register{Module}Routes(r gin.IRouter, app *bootstrap.App)
55
+ │ │ │ └── {module}_dtos.go
56
+ ├── services/
57
+ │ │ └── {module}_service.go
58
+ ├── listeners/
59
+ │ │ └── on_{event_name}.go
60
+ └── eventbus/
61
+ ├── dispatcher.go
62
+ │ │ └── registry.go
63
+ │ ├── infrastructure/
64
+ │ │ ├── database/
65
+ │ │ │ ├── mysql.go
66
+ │ │ │ └── {domain}_{entity}_store.go
67
+ │ │ ├── http/
68
+ │ │ │ ├── response.go
69
+ │ │ │ └── pagination.go
70
+ │ │ ├── cache/
71
+ │ │ ├── messaging/
72
+ │ │ ├── auth/
73
+ │ │ └── logger/
74
+ │ ├── models/ # GORM models (DB representation)
75
+ │ │ └── {entity}.go
76
+ │ ├── bootstrap/
77
+ │ │ └── app.go
78
+ │ └── config/
79
+ │ └── config.go
80
+ ├── config.yaml
61
81
  ├── go.mod
62
82
  └── go.sum
63
83
  ```
64
84
 
65
- ## Key Patterns and Conventions
85
+ ## Key Patterns
66
86
 
67
- ### File Naming
68
- - Use `snake_case.go` for all Go files
69
- - One struct per file when possible
87
+ ### Entity Pattern
88
+
89
+ Entities live in `internal/domain/{domain}/entities/`. They hold behavior, embed `EventCollector`, and are created via a `New()` constructor.
90
+
91
+ ```go
92
+ // internal/domain/order/entities/order.go
93
+ package entities
94
+
95
+ import (
96
+ "time"
97
+
98
+ "github.com/google/uuid"
99
+ "yourproject/internal/domain/order/events"
100
+ "yourproject/internal/domain/order/valueobjects"
101
+ "yourproject/internal/domain/shared"
102
+ )
103
+
104
+ type Order struct {
105
+ shared.EventCollector
106
+ ID string
107
+ CustomerID string
108
+ Status valueobjects.OrderStatus
109
+ Total valueobjects.Money
110
+ Items []OrderItem
111
+ CreatedAt time.Time
112
+ UpdatedAt time.Time
113
+ }
70
114
 
71
- ### Module Structure
72
- Each module follows the layered architecture:
73
- - **controllers/**: HTTP handlers, request/response mapping
74
- - **usecases/**: Business logic
75
- - **models/**: GORM models and DTOs
115
+ func New(customerID string, items []OrderItem, total valueobjects.Money) *Order {
116
+ id := uuid.New().String()
117
+ o := &Order{
118
+ ID: id,
119
+ CustomerID: customerID,
120
+ Status: valueobjects.OrderStatusPending,
121
+ Total: total,
122
+ Items: items,
123
+ CreatedAt: time.Now(),
124
+ UpdatedAt: time.Now(),
125
+ }
126
+ o.Record(events.NewOrderCreated(id, customerID, total.Amount()))
127
+ return o
128
+ }
129
+
130
+ func (o *Order) Confirm() error {
131
+ if o.Status != valueobjects.OrderStatusPending {
132
+ return ErrOrderNotPending
133
+ }
134
+ o.Status = valueobjects.OrderStatusConfirmed
135
+ o.UpdatedAt = time.Now()
136
+ o.Record(events.NewOrderConfirmed(o.ID))
137
+ return nil
138
+ }
139
+
140
+ func (o *Order) Cancel(reason string) error {
141
+ if o.Status == valueobjects.OrderStatusCancelled {
142
+ return ErrOrderAlreadyCancelled
143
+ }
144
+ o.Status = valueobjects.OrderStatusCancelled
145
+ o.UpdatedAt = time.Now()
146
+ o.Record(events.NewOrderCancelled(o.ID, reason))
147
+ return nil
148
+ }
149
+ ```
76
150
 
77
- ### Module Init Pattern
151
+ ### Shared EventCollector
78
152
 
79
153
  ```go
80
- // internal/modules/{module}/init.go
81
- package module
154
+ // internal/domain/shared/event_collector.go
155
+ package shared
82
156
 
83
- func Init(r *gin.Engine, db *gorm.DB) {
84
- repo := NewRepository(db)
85
- uc := NewUseCase(repo)
86
- ctrl := NewController(uc)
157
+ type Event interface {
158
+ EventName() string
159
+ }
160
+
161
+ type BaseEvent struct {
162
+ Name string
163
+ OccurredAt int64
164
+ }
165
+
166
+ func (e BaseEvent) EventName() string { return e.Name }
167
+
168
+ type EventCollector struct {
169
+ events []Event
170
+ }
87
171
 
88
- group := r.Group("/api/{module}")
89
- {
90
- group.GET("/", ctrl.List)
91
- group.GET("/:id", ctrl.Get)
92
- group.POST("/", ctrl.Create)
93
- group.PUT("/:id", ctrl.Update)
94
- group.DELETE("/:id", ctrl.Delete)
95
- }
172
+ func (ec *EventCollector) Record(e Event) {
173
+ ec.events = append(ec.events, e)
174
+ }
175
+
176
+ func (ec *EventCollector) PullEvents() []Event {
177
+ evts := ec.events
178
+ ec.events = nil
179
+ return evts
96
180
  }
97
181
  ```
98
182
 
99
- ### Response Format
183
+ ### Value Object Pattern
184
+
185
+ Value objects live in `internal/domain/{domain}/valueobjects/`. They use only stdlib, are immutable, and validate on construction.
100
186
 
101
187
  ```go
102
- // Success response
103
- c.JSON(http.StatusOK, gin.H{
104
- "data": result,
105
- })
188
+ // internal/domain/order/valueobjects/money.go
189
+ package valueobjects
106
190
 
107
- // Error response
108
- c.JSON(http.StatusBadRequest, gin.H{
109
- "error": "validation failed",
110
- "details": errors,
111
- })
191
+ import "errors"
112
192
 
113
- // Paginated response
114
- c.JSON(http.StatusOK, gin.H{
115
- "data": items,
116
- "total": total,
117
- "page": page,
118
- "limit": limit,
119
- "total_pages": totalPages,
120
- })
193
+ var ErrNegativeAmount = errors.New("amount must not be negative")
194
+
195
+ type Money struct {
196
+ amount float64
197
+ currency string
198
+ }
199
+
200
+ func NewMoney(amount float64, currency string) (Money, error) {
201
+ if amount < 0 {
202
+ return Money{}, ErrNegativeAmount
203
+ }
204
+ return Money{amount: amount, currency: currency}, nil
205
+ }
206
+
207
+ func (m Money) Amount() float64 { return m.amount }
208
+ func (m Money) Currency() string { return m.currency }
209
+
210
+ func (m Money) Add(other Money) (Money, error) {
211
+ if m.currency != other.currency {
212
+ return Money{}, errors.New("currency mismatch")
213
+ }
214
+ return NewMoney(m.amount+other.amount, m.currency)
215
+ }
121
216
  ```
122
217
 
123
- ### GORM Model Pattern
218
+ ### Port Pattern
219
+
220
+ Ports live in `internal/domain/{domain}/ports/`. One interface per file. Domain defines what it needs, infrastructure implements it.
124
221
 
125
222
  ```go
126
- type Entity struct {
127
- ID string `gorm:"type:char(36);primaryKey" json:"id"`
128
- Name string `gorm:"type:varchar(255);not null" json:"name"`
129
- Status string `gorm:"type:varchar(50);default:'active'" json:"status"`
130
- CreatedAt time.Time `json:"created_at"`
131
- UpdatedAt time.Time `json:"updated_at"`
223
+ // internal/domain/order/ports/order_store.go
224
+ package ports
225
+
226
+ import "yourproject/internal/domain/order/entities"
227
+
228
+ type OrderStore interface {
229
+ FindByID(id string) (*entities.Order, error)
230
+ FindByCustomerID(customerID string, page, limit int) ([]*entities.Order, int64, error)
231
+ Save(order *entities.Order) error
232
+ Update(order *entities.Order) error
132
233
  }
234
+ ```
235
+
236
+ ### Event Pattern
133
237
 
134
- func (e *Entity) BeforeCreate(tx *gorm.DB) error {
135
- e.ID = uuid.New().String()
136
- return nil
238
+ Events live in `internal/domain/{domain}/events/`. One event per file, embed `BaseEvent`.
239
+
240
+ ```go
241
+ // internal/domain/order/events/order_created.go
242
+ package events
243
+
244
+ import (
245
+ "time"
246
+ "yourproject/internal/domain/shared"
247
+ )
248
+
249
+ type OrderCreated struct {
250
+ shared.BaseEvent
251
+ OrderID string
252
+ CustomerID string
253
+ Total float64
254
+ }
255
+
256
+ func NewOrderCreated(orderID, customerID string, total float64) OrderCreated {
257
+ return OrderCreated{
258
+ BaseEvent: shared.BaseEvent{Name: "order.created", OccurredAt: time.Now().Unix()},
259
+ OrderID: orderID,
260
+ CustomerID: customerID,
261
+ Total: total,
262
+ }
137
263
  }
138
264
  ```
139
265
 
140
- ## Adding New Module
266
+ ### UseCase Pattern
141
267
 
142
- 1. Create module directory structure:
143
- ```bash
144
- mkdir -p internal/modules/{module}/{controllers,models,usecases}
268
+ Usecases live in `internal/domain/{domain}/usecases/`. One action per file. They depend only on ports (interfaces), never on infrastructure.
269
+
270
+ ```go
271
+ // internal/domain/order/usecases/create_order.go
272
+ package usecases
273
+
274
+ import (
275
+ "yourproject/internal/domain/order/entities"
276
+ "yourproject/internal/domain/order/ports"
277
+ "yourproject/internal/domain/order/valueobjects"
278
+ )
279
+
280
+ type CreateOrder struct {
281
+ orderStore ports.OrderStore
282
+ }
283
+
284
+ func NewCreateOrder(orderStore ports.OrderStore) *CreateOrder {
285
+ return &CreateOrder{orderStore: orderStore}
286
+ }
287
+
288
+ type CreateOrderInput struct {
289
+ CustomerID string
290
+ Items []entities.OrderItem
291
+ Total float64
292
+ Currency string
293
+ }
294
+
295
+ func (uc *CreateOrder) Execute(input CreateOrderInput) (*entities.Order, error) {
296
+ total, err := valueobjects.NewMoney(input.Total, input.Currency)
297
+ if err != nil {
298
+ return nil, err
299
+ }
300
+
301
+ order := entities.New(input.CustomerID, input.Items, total)
302
+ if err := uc.orderStore.Save(order); err != nil {
303
+ return nil, err
304
+ }
305
+ return order, nil
306
+ }
307
+ ```
308
+
309
+ ### Handler Wiring Pattern
310
+
311
+ Handlers live in `internal/application/ports/http/`. Each module exposes `Register{Module}Routes` that receives `gin.IRouter` and `*bootstrap.App`.
312
+
313
+ ```go
314
+ // internal/application/ports/http/order_handler.go
315
+ package http
316
+
317
+ import (
318
+ "net/http"
319
+
320
+ "github.com/gin-gonic/gin"
321
+ "yourproject/internal/bootstrap"
322
+ "yourproject/internal/domain/order/usecases"
323
+ )
324
+
325
+ func RegisterOrderRoutes(r gin.IRouter, app *bootstrap.App) {
326
+ h := &orderHandler{
327
+ createOrder: usecases.NewCreateOrder(app.OrderStore),
328
+ }
329
+
330
+ g := r.Group("/orders")
331
+ {
332
+ g.POST("/", h.Create)
333
+ g.GET("/:id", h.GetByID)
334
+ g.GET("/", h.List)
335
+ g.PUT("/:id/confirm", h.Confirm)
336
+ g.PUT("/:id/cancel", h.Cancel)
337
+ }
338
+ }
339
+
340
+ type orderHandler struct {
341
+ createOrder *usecases.CreateOrder
342
+ }
343
+
344
+ func (h *orderHandler) Create(c *gin.Context) {
345
+ var dto CreateOrderDTO
346
+ if err := c.ShouldBindJSON(&dto); err != nil {
347
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
348
+ return
349
+ }
350
+
351
+ order, err := h.createOrder.Execute(dto.ToInput())
352
+ if err != nil {
353
+ c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
354
+ return
355
+ }
356
+ c.JSON(http.StatusCreated, gin.H{"data": order})
357
+ }
145
358
  ```
146
359
 
147
- 2. Create model (`models/entity.go`):
360
+ ### DTOs
361
+
148
362
  ```go
149
- type Entity struct {
150
- ID string `gorm:"primaryKey" json:"id"`
151
- Name string `json:"name"`
363
+ // internal/application/ports/http/order_dtos.go
364
+ package http
365
+
366
+ import "yourproject/internal/domain/order/usecases"
367
+
368
+ type CreateOrderDTO struct {
369
+ CustomerID string `json:"customer_id" binding:"required"`
370
+ Items []OrderItemDTO `json:"items" binding:"required,dive"`
371
+ Currency string `json:"currency" binding:"required"`
372
+ }
373
+
374
+ func (d CreateOrderDTO) ToInput() usecases.CreateOrderInput {
375
+ // map DTO to usecase input
376
+ return usecases.CreateOrderInput{
377
+ CustomerID: d.CustomerID,
378
+ Currency: d.Currency,
379
+ }
152
380
  }
153
381
  ```
154
382
 
155
- 3. Create usecase (`usecases/usecase.go`):
383
+ ### Store Pattern (Infrastructure)
384
+
385
+ Stores live in `internal/infrastructure/database/`. They implement domain ports using GORM models.
386
+
156
387
  ```go
157
- type UseCase struct {
158
- db *gorm.DB
388
+ // internal/infrastructure/database/order_order_store.go
389
+ package database
390
+
391
+ import (
392
+ "gorm.io/gorm"
393
+ "yourproject/internal/domain/order/entities"
394
+ "yourproject/internal/domain/order/ports"
395
+ "yourproject/internal/models"
396
+ )
397
+
398
+ type orderStore struct {
399
+ db *gorm.DB
400
+ }
401
+
402
+ func NewOrderStore(db *gorm.DB) ports.OrderStore {
403
+ return &orderStore{db: db}
404
+ }
405
+
406
+ func (s *orderStore) FindByID(id string) (*entities.Order, error) {
407
+ var m models.Order
408
+ if err := s.db.First(&m, "id = ?", id).Error; err != nil {
409
+ return nil, err
410
+ }
411
+ return m.ToEntity(), nil
412
+ }
413
+
414
+ func (s *orderStore) Save(order *entities.Order) error {
415
+ m := models.OrderFromEntity(order)
416
+ return s.db.Create(&m).Error
159
417
  }
160
418
 
161
- func (uc *UseCase) List() ([]models.Entity, error) {
162
- var items []models.Entity
163
- return items, uc.db.Find(&items).Error
419
+ func (s *orderStore) Update(order *entities.Order) error {
420
+ m := models.OrderFromEntity(order)
421
+ return s.db.Save(&m).Error
422
+ }
423
+
424
+ func (s *orderStore) FindByCustomerID(customerID string, page, limit int) ([]*entities.Order, int64, error) {
425
+ var items []models.Order
426
+ var total int64
427
+
428
+ s.db.Model(&models.Order{}).Where("customer_id = ?", customerID).Count(&total)
429
+ err := s.db.Where("customer_id = ?", customerID).
430
+ Offset((page - 1) * limit).Limit(limit).
431
+ Find(&items).Error
432
+
433
+ result := make([]*entities.Order, len(items))
434
+ for i, m := range items {
435
+ result[i] = m.ToEntity()
436
+ }
437
+ return result, total, err
164
438
  }
165
439
  ```
166
440
 
167
- 4. Create controller (`controllers/controller.go`):
441
+ ### GORM Model Pattern
442
+
443
+ Models live in `internal/models/`. They are pure GORM structs with `ToEntity()` and `FromEntity()` converters.
444
+
168
445
  ```go
169
- type Controller struct {
170
- uc *usecases.UseCase
446
+ // internal/models/order.go
447
+ package models
448
+
449
+ import (
450
+ "time"
451
+
452
+ "yourproject/internal/domain/order/entities"
453
+ "yourproject/internal/domain/order/valueobjects"
454
+ )
455
+
456
+ type Order struct {
457
+ ID string `gorm:"type:char(36);primaryKey" json:"id"`
458
+ CustomerID string `gorm:"type:char(36);not null;index" json:"customer_id"`
459
+ Status string `gorm:"type:varchar(50);default:'pending'" json:"status"`
460
+ Total float64 `gorm:"type:decimal(10,2)" json:"total"`
461
+ Currency string `gorm:"type:varchar(10)" json:"currency"`
462
+ CreatedAt time.Time `json:"created_at"`
463
+ UpdatedAt time.Time `json:"updated_at"`
464
+ }
465
+
466
+ func (m *Order) ToEntity() *entities.Order {
467
+ total, _ := valueobjects.NewMoney(m.Total, m.Currency)
468
+ return &entities.Order{
469
+ ID: m.ID,
470
+ CustomerID: m.CustomerID,
471
+ Status: valueobjects.OrderStatus(m.Status),
472
+ Total: total,
473
+ CreatedAt: m.CreatedAt,
474
+ UpdatedAt: m.UpdatedAt,
475
+ }
171
476
  }
172
477
 
173
- func (ctrl *Controller) List(c *gin.Context) {
174
- items, err := ctrl.uc.List()
175
- if err != nil {
176
- c.JSON(500, gin.H{"error": err.Error()})
177
- return
178
- }
179
- c.JSON(200, gin.H{"data": items})
478
+ func OrderFromEntity(e *entities.Order) Order {
479
+ return Order{
480
+ ID: e.ID,
481
+ CustomerID: e.CustomerID,
482
+ Status: string(e.Status),
483
+ Total: e.Total.Amount(),
484
+ Currency: e.Total.Currency(),
485
+ CreatedAt: e.CreatedAt,
486
+ UpdatedAt: e.UpdatedAt,
487
+ }
180
488
  }
181
489
  ```
182
490
 
183
- 5. Create init.go and register in router
491
+ ### Bootstrap Pattern
492
+
493
+ ```go
494
+ // internal/bootstrap/app.go
495
+ package bootstrap
496
+
497
+ import (
498
+ "yourproject/internal/domain/order/ports"
499
+ )
500
+
501
+ type App struct {
502
+ OrderStore ports.OrderStore
503
+ // add more stores/services here
504
+ }
505
+ ```
506
+
507
+ ### Listener Pattern
508
+
509
+ Listeners live in `internal/application/listeners/`. They react to domain events.
510
+
511
+ ```go
512
+ // internal/application/listeners/on_order_created.go
513
+ package listeners
514
+
515
+ import (
516
+ "log"
517
+ "yourproject/internal/domain/order/events"
518
+ "yourproject/internal/domain/shared"
519
+ )
520
+
521
+ type OnOrderCreated struct{}
522
+
523
+ func (l *OnOrderCreated) Handle(evt shared.Event) {
524
+ e := evt.(events.OrderCreated)
525
+ log.Printf("Order created: %s for customer %s, total: %.2f", e.OrderID, e.CustomerID, e.Total)
526
+ // send email, update analytics, etc.
527
+ }
528
+ ```
529
+
530
+ ### EventBus Pattern
531
+
532
+ ```go
533
+ // internal/application/eventbus/dispatcher.go
534
+ package eventbus
535
+
536
+ import "yourproject/internal/domain/shared"
537
+
538
+ type Handler interface {
539
+ Handle(event shared.Event)
540
+ }
541
+
542
+ type Dispatcher struct {
543
+ handlers map[string][]Handler
544
+ }
545
+
546
+ func NewDispatcher() *Dispatcher {
547
+ return &Dispatcher{handlers: make(map[string][]Handler)}
548
+ }
549
+
550
+ func (d *Dispatcher) Register(eventName string, h Handler) {
551
+ d.handlers[eventName] = append(d.handlers[eventName], h)
552
+ }
553
+
554
+ func (d *Dispatcher) Dispatch(events []shared.Event) {
555
+ for _, evt := range events {
556
+ for _, h := range d.handlers[evt.EventName()] {
557
+ go h.Handle(evt)
558
+ }
559
+ }
560
+ }
561
+ ```
562
+
563
+ ### Router Pattern
564
+
565
+ ```go
566
+ // cmd/v1/router.go
567
+ package main
568
+
569
+ import (
570
+ "github.com/gin-gonic/gin"
571
+ "yourproject/internal/bootstrap"
572
+ apphttp "yourproject/internal/application/ports/http"
573
+ )
574
+
575
+ func setupRouter(app *bootstrap.App) *gin.Engine {
576
+ r := gin.Default()
577
+
578
+ v1 := r.Group("/api/v1")
579
+ apphttp.RegisterOrderRoutes(v1, app)
580
+ // apphttp.RegisterProductRoutes(v1, app)
581
+
582
+ return r
583
+ }
584
+ ```
585
+
586
+ ## Adding a New Domain Module
587
+
588
+ ### Step 1: Domain Layer (no dependencies)
589
+
590
+ ```bash
591
+ mkdir -p internal/domain/{domain}/{entities,valueobjects,ports,events,usecases}
592
+ ```
593
+
594
+ 1. Define **value objects** in `valueobjects/` (validate on construction, stdlib only)
595
+ 2. Define **entity** in `entities/` with `New()` constructor, behavior methods, embed `EventCollector`
596
+ 3. Define **events** in `events/` (one per file, embed `BaseEvent`)
597
+ 4. Define **port interfaces** in `ports/` (one per file)
598
+ 5. Write **usecases** in `usecases/` (one action per file, depend on ports only)
599
+
600
+ ### Step 2: Infrastructure Layer
601
+
602
+ 1. Create GORM model in `internal/models/` with `ToEntity()` / `FromEntity()`
603
+ 2. Implement store in `internal/infrastructure/database/` satisfying the domain port
604
+ 3. Add `AutoMigrate(&models.YourEntity{})` in `cmd/v1/main.go`
605
+
606
+ ### Step 3: Application Layer
607
+
608
+ 1. Add store field to `internal/bootstrap/app.go`
609
+ 2. Create handler in `internal/application/ports/http/` with `Register{Module}Routes`
610
+ 3. Create DTOs in `internal/application/ports/http/{module}_dtos.go`
611
+ 4. Register routes in `cmd/v1/router.go`
612
+ 5. (Optional) Add listeners in `internal/application/listeners/`
613
+
614
+ ## Import Rules
615
+
616
+ Domain purity is enforced by convention:
617
+
618
+ | Layer | Can Import | CANNOT Import |
619
+ |-------|-----------|---------------|
620
+ | `domain/` | Only stdlib + `domain/shared/` | `application/`, `infrastructure/`, `models/`, any external lib |
621
+ | `domain/{x}/usecases/` | `domain/{x}/entities/`, `domain/{x}/ports/`, `domain/{x}/valueobjects/`, `domain/{x}/events/` | Anything outside its own domain |
622
+ | `application/` | `domain/`, `bootstrap/`, `infrastructure/` (for wiring only) | — |
623
+ | `infrastructure/` | `domain/ports/`, `domain/entities/`, `models/` | `application/` |
624
+ | `models/` | `domain/entities/`, `domain/valueobjects/` | `application/`, `infrastructure/` |
625
+
626
+ **Golden rule**: If you need to add `gorm.io` or any external import to a file under `domain/`, you are doing it wrong.
184
627
 
185
628
  ## API Endpoints Pattern
186
629
 
187
630
  | Method | Path | Description |
188
631
  |--------|------|-------------|
189
- | GET | /api/{resource}/ | List all items |
190
- | GET | /api/{resource}/:id | Get single item |
191
- | POST | /api/{resource}/ | Create item |
192
- | PUT | /api/{resource}/:id | Update item |
193
- | DELETE | /api/{resource}/:id | Delete item |
632
+ | GET | /api/v1/{resource}/ | List items (paginated) |
633
+ | GET | /api/v1/{resource}/:id | Get single item |
634
+ | POST | /api/v1/{resource}/ | Create item |
635
+ | PUT | /api/v1/{resource}/:id | Update item |
636
+ | PUT | /api/v1/{resource}/:id/{action} | Trigger domain action |
637
+ | DELETE | /api/v1/{resource}/:id | Delete item |
194
638
 
195
639
  ### Query Parameters for List
196
640
  - `page` - Page number (default: 1)
@@ -199,6 +643,27 @@ func (ctrl *Controller) List(c *gin.Context) {
199
643
  - `sort_by` - Sort field
200
644
  - `order` - Sort order (asc/desc)
201
645
 
646
+ ### Response Format
647
+
648
+ ```go
649
+ // internal/infrastructure/http/response.go
650
+
651
+ // Success
652
+ c.JSON(http.StatusOK, gin.H{"data": result})
653
+
654
+ // Error
655
+ c.JSON(http.StatusBadRequest, gin.H{"error": "validation failed", "details": errors})
656
+
657
+ // Paginated
658
+ c.JSON(http.StatusOK, gin.H{
659
+ "data": items,
660
+ "total": total,
661
+ "page": page,
662
+ "limit": limit,
663
+ "total_pages": totalPages,
664
+ })
665
+ ```
666
+
202
667
  ## Configuration
203
668
 
204
669
  ### config.yaml
@@ -231,14 +696,99 @@ jwt:
231
696
 
232
697
  ## Testing
233
698
 
699
+ ### Domain Unit Tests (no mocks needed)
700
+
701
+ ```go
702
+ // internal/domain/order/entities/order_test.go
703
+ func TestNewOrder(t *testing.T) {
704
+ total, _ := valueobjects.NewMoney(100, "USD")
705
+ order := entities.New("customer-1", nil, total)
706
+
707
+ assert.NotEmpty(t, order.ID)
708
+ assert.Equal(t, valueobjects.OrderStatusPending, order.Status)
709
+
710
+ events := order.PullEvents()
711
+ assert.Len(t, events, 1)
712
+ assert.Equal(t, "order.created", events[0].EventName())
713
+ }
714
+
715
+ func TestOrder_Confirm(t *testing.T) {
716
+ total, _ := valueobjects.NewMoney(100, "USD")
717
+ order := entities.New("customer-1", nil, total)
718
+ order.PullEvents() // clear creation events
719
+
720
+ err := order.Confirm()
721
+ assert.NoError(t, err)
722
+ assert.Equal(t, valueobjects.OrderStatusConfirmed, order.Status)
723
+
724
+ events := order.PullEvents()
725
+ assert.Len(t, events, 1)
726
+ assert.Equal(t, "order.confirmed", events[0].EventName())
727
+ }
728
+
729
+ func TestOrder_Confirm_WhenNotPending_ReturnsError(t *testing.T) {
730
+ total, _ := valueobjects.NewMoney(100, "USD")
731
+ order := entities.New("customer-1", nil, total)
732
+ _ = order.Confirm()
733
+
734
+ err := order.Confirm()
735
+ assert.ErrorIs(t, err, entities.ErrOrderNotPending)
736
+ }
737
+ ```
738
+
739
+ ### Value Object Tests
740
+
741
+ ```go
742
+ // internal/domain/order/valueobjects/money_test.go
743
+ func TestNewMoney_NegativeAmount(t *testing.T) {
744
+ _, err := valueobjects.NewMoney(-10, "USD")
745
+ assert.ErrorIs(t, err, valueobjects.ErrNegativeAmount)
746
+ }
747
+
748
+ func TestMoney_Add_CurrencyMismatch(t *testing.T) {
749
+ usd, _ := valueobjects.NewMoney(10, "USD")
750
+ eur, _ := valueobjects.NewMoney(5, "EUR")
751
+ _, err := usd.Add(eur)
752
+ assert.Error(t, err)
753
+ }
754
+ ```
755
+
756
+ ### UseCase Tests (mock ports)
757
+
234
758
  ```go
235
- // Use testify for assertions
236
- func TestController_List(t *testing.T) {
237
- router := setupTestRouter()
238
- w := httptest.NewRecorder()
239
- req, _ := http.NewRequest("GET", "/api/items/", nil)
240
- router.ServeHTTP(w, req)
759
+ // internal/domain/order/usecases/create_order_test.go
760
+ type mockOrderStore struct {
761
+ saved *entities.Order
762
+ }
763
+
764
+ func (m *mockOrderStore) Save(o *entities.Order) error {
765
+ m.saved = o
766
+ return nil
767
+ }
768
+ func (m *mockOrderStore) FindByID(id string) (*entities.Order, error) { return nil, nil }
769
+ func (m *mockOrderStore) Update(o *entities.Order) error { return nil }
770
+ func (m *mockOrderStore) FindByCustomerID(string, int, int) ([]*entities.Order, int64, error) {
771
+ return nil, 0, nil
772
+ }
773
+
774
+ func TestCreateOrder_Execute(t *testing.T) {
775
+ store := &mockOrderStore{}
776
+ uc := usecases.NewCreateOrder(store)
777
+
778
+ order, err := uc.Execute(usecases.CreateOrderInput{
779
+ CustomerID: "cust-1",
780
+ Total: 99.99,
781
+ Currency: "USD",
782
+ })
241
783
 
242
- assert.Equal(t, 200, w.Code)
784
+ assert.NoError(t, err)
785
+ assert.NotNil(t, order)
786
+ assert.NotNil(t, store.saved)
787
+ assert.Equal(t, "cust-1", store.saved.CustomerID)
243
788
  }
244
789
  ```
790
+
791
+ ### File Naming
792
+ - Use `snake_case.go` for all Go files
793
+ - One struct/interface per file when possible
794
+ - Test files: `{name}_test.go` in the same package