guardian-framework 0.1.10 → 0.1.12

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.
@@ -0,0 +1,450 @@
1
+ # Go Enterprise Code Generation — DDD + Clean Architecture
2
+
3
+ > Canonical skill for generating production-grade Go code following Clean Architecture with DDD.
4
+ > All code MUST follow these patterns. Validators enforce compliance.
5
+ >
6
+ > Source: Clean Architecture principles + Go standard library patterns + DDD patterns from context7.
7
+
8
+ ---
9
+
10
+ ## 1. Project Structure — Clean Architecture with DDD
11
+
12
+ Every bounded context follows the same 4-layer structure:
13
+
14
+ ```
15
+ module/
16
+ ├── domain/ # Pure domain entities, value objects, events
17
+ │ ├── entity.go # Aggregate roots and entities
18
+ │ ├── value.go # Value objects
19
+ │ ├── event.go # Domain event payloads
20
+ │ ├── error.go # Typed error definitions
21
+ │ └── repository.go # Repository interfaces
22
+ ├── application/ # Service interfaces and DTOs
23
+ │ ├── service.go # Service interface definitions
24
+ │ └── dto.go # Input/Output DTOs
25
+ ├── infrastructure/ # Repository implementations, external adapters
26
+ │ ├── repository/ # Repository implementations (GORM, SQL)
27
+ │ │ ├── module_repo.go
28
+ │ │ └── module_test.go
29
+ │ └── persistence/ # Database connection, migrations
30
+ └── interfaces/ # API contracts (HTTP)
31
+ ├── handler.go # HTTP handler definitions
32
+ ├── router.go # Route registration
33
+ └── dto.go # Request/Response DTOs
34
+ ```
35
+
36
+ ### Dependency Direction Rule (Inward Dependency)
37
+
38
+ ```
39
+ domain → application → infrastructure → interfaces
40
+ ↑ ↑
41
+ └── inner layers never depend on outer layers
42
+ ```
43
+
44
+ - **domain/** — depends on nothing except standard library
45
+ - **application/** — depends on domain
46
+ - **infrastructure/** — depends on domain + application
47
+ - **interfaces/** — depends on application (HTTP/event handlers translate to domain calls)
48
+
49
+ ### Package Naming Convention
50
+
51
+ ```go
52
+ package module // domain/ — single domain package
53
+ package application // application/
54
+ package repository // infrastructure/repository/
55
+ package handler // interfaces/
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 2. Domain Layer Entities
61
+
62
+ ### Entity with Encapsulated State
63
+
64
+ ```go
65
+ // entity.go — Domain entity with encapsulate state and behavior.
66
+
67
+ // Entity is a domain object with identity (ID field).
68
+ type Entity struct {
69
+ id primitive.ObjectID
70
+ status Status
71
+ createdAt time.Time
72
+ updatedAt time.Time
73
+ }
74
+
75
+ func NewEntity(id primitive.ObjectID) *Entity {
76
+ return &Entity{
77
+ id: id,
78
+ status: StatusPending,
79
+ createdAt: time.Now(),
80
+ updatedAt: time.Now(),
81
+ }
82
+ }
83
+
84
+ // ID returns the immutable entity identifier.
85
+ func (e *Entity) ID() primitive.ObjectID { return e.id }
86
+
87
+ // Status returns the current lifecycle status.
88
+ func (e *Entity) Status() Status { return e.status }
89
+
90
+ // transition encapsulates state mutations — never expose setters.
91
+ func (e *Entity) transition(newStatus Status) error {
92
+ if !e.status.CanTransitionTo(newStatus) {
93
+ return &DomainError{
94
+ Code: ErrInvalidTransition,
95
+ Message: fmt.Sprintf("cannot transition from %s to %s", e.status, newStatus),
96
+ }
97
+ }
98
+ e.status = newStatus
99
+ e.updatedAt = time.Now()
100
+ return nil
101
+ }
102
+ ```
103
+
104
+ ### Value Object
105
+
106
+ ```go
107
+ // value.go — Immutable value object.
108
+
109
+ // Money represents an amount in the smallest currency unit.
110
+ type Money struct {
111
+ amount int64
112
+ currency string
113
+ }
114
+
115
+ func NewMoney(amount int64, currency string) (Money, error) {
116
+ if amount < 0 {
117
+ return Money{}, &DomainError{Code: ErrValidation, Message: "amount must be non-negative"}
118
+ }
119
+ if len(currency) != 3 {
120
+ return Money{}, &DomainError{Code: ErrValidation, Message: "currency must be ISO 4217"}
121
+ }
122
+ return Money{amount: amount, currency: strings.ToUpper(currency)}, nil
123
+ }
124
+
125
+ func (m Money) Amount() int64 { return m.amount }
126
+ func (m Money) Currency() string { return m.currency }
127
+ func (m Money) Add(other Money) (Money, error) {
128
+ if m.currency != other.currency {
129
+ return Money{}, &DomainError{Code: ErrCurrencyMismatch, Message: "currency mismatch"}
130
+ }
131
+ return Money{amount: m.amount + other.amount, currency: m.currency}, nil
132
+ }
133
+ ```
134
+
135
+ ### Repository Interface
136
+
137
+ ```go
138
+ // repository.go — Repository interface defined in domain, implemented in infrastructure.
139
+
140
+ type Repository interface {
141
+ FindByID(ctx context.Context, id primitive.ObjectID) (*Entity, error)
142
+ Save(ctx context.Context, entity *Entity) error
143
+ Delete(ctx context.Context, id primitive.ObjectID) error
144
+ FindByStatus(ctx context.Context, status Status) ([]*Entity, error)
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 3. Domain Error Handling
151
+
152
+ ```go
153
+ // error.go — Typed domain errors.
154
+
155
+ type ErrorCode string
156
+
157
+ const (
158
+ ErrNotFound ErrorCode = "NOT_FOUND"
159
+ ErrInvalidState ErrorCode = "INVALID_STATE"
160
+ ErrValidation ErrorCode = "VALIDATION"
161
+ ErrDuplicate ErrorCode = "DUPLICATE"
162
+ ErrCurrencyMismatch ErrorCode = "CURRENCY_MISMATCH"
163
+ ErrInvalidTransition ErrorCode = "INVALID_TRANSITION"
164
+ )
165
+
166
+ type DomainError struct {
167
+ Code ErrorCode
168
+ Message string
169
+ Err error
170
+ }
171
+
172
+ func (e *DomainError) Error() string {
173
+ if e.Err != nil {
174
+ return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
175
+ }
176
+ return fmt.Sprintf("[%s] %s", e.Code, e.Message)
177
+ }
178
+
179
+ func (e *DomainError) Unwrap() error { return e.Err }
180
+
181
+ // Sentinel errors for direct comparison.
182
+ var (
183
+ ErrNotFoundInstance = &DomainError{Code: ErrNotFound, Message: "resource not found"}
184
+ ErrDuplicateInstance = &DomainError{Code: ErrDuplicate, Message: "duplicate resource"}
185
+ )
186
+ ```
187
+
188
+ ### Rules
189
+ - ✅ Use `*DomainError` with typed `ErrorCode` for all domain errors
190
+ - ✅ Sentinel errors for direct comparison via `errors.Is()`
191
+ - ✅ Use `fmt.Errorf("context: %w", err)` for wrapping with context
192
+ - ✅ Use `errors.As()` to extract typed errors from wrapped chains
193
+ - ❌ Never use `panic` for error handling
194
+ - ❌ Never use bare strings as errors
195
+ - ❌ Never use `log.Fatal` in library code
196
+
197
+ ---
198
+
199
+ ## 4. Application Layer — Services
200
+
201
+ ```go
202
+ // service.go — Application service interface.
203
+
204
+ type Service interface {
205
+ Execute(ctx context.Context, cmd Command) (*Entity, error)
206
+ GetByID(ctx context.Context, id primitive.ObjectID) (*Entity, error)
207
+ }
208
+
209
+ type service struct {
210
+ repo Repository
211
+ logger *slog.Logger
212
+ }
213
+
214
+ func NewService(repo Repository, logger *slog.Logger) Service {
215
+ return &service{repo: repo, logger: logger}
216
+ }
217
+
218
+ func (s *service) Execute(ctx context.Context, cmd Command) (*Entity, error) {
219
+ entity := NewEntity(primitive.NewObjectID())
220
+ // Domain logic — validation, state transitions
221
+ if err := entity.transition(cmd.DesiredStatus); err != nil {
222
+ return nil, fmt.Errorf("execute: %w", err)
223
+ }
224
+ if err := s.repo.Save(ctx, entity); err != nil {
225
+ return nil, fmt.Errorf("save entity: %w", err)
226
+ }
227
+ s.logger.InfoContext(ctx, "entity executed", "id", entity.ID())
228
+ return entity, nil
229
+ }
230
+ ```
231
+
232
+ ### Rules
233
+ - ✅ Services orchestrate domain operations — no business logic
234
+ - ✅ Services depend on domain interfaces (Repository), not concrete implementations
235
+ - ✅ Use `slog.Logger` for structured logging
236
+ - ✅ Each service method is a single use case
237
+
238
+ ---
239
+
240
+ ## 5. Infrastructure Layer — Repository Implementation
241
+
242
+ ```go
243
+ // repository/module_repo.go — GORM-based repository implementation.
244
+
245
+ type gormRepository struct {
246
+ db *gorm.DB
247
+ logger *slog.Logger
248
+ }
249
+
250
+ func NewGormRepository(db *gorm.DB, logger *slog.Logger) Repository {
251
+ return &gormRepository{db: db, logger: logger}
252
+ }
253
+
254
+ func (r *gormRepository) FindByID(ctx context.Context, id primitive.ObjectID) (*Entity, error) {
255
+ var model entityModel
256
+ if err := r.db.WithContext(ctx).First(&model, "id = ?", id).Error; err != nil {
257
+ if errors.Is(err, gorm.ErrRecordNotFound) {
258
+ return nil, fmt.Errorf("find by id %s: %w", id.Hex(), ErrNotFoundInstance)
259
+ }
260
+ return nil, fmt.Errorf("find by id %s: %w", id.Hex(), err)
261
+ }
262
+ return model.toEntity(), nil
263
+ }
264
+ ```
265
+
266
+ ### Rules
267
+ - ✅ Repository implementations live in `infrastructure/repository/`
268
+ - ✅ Each repository implements interfaces defined in `domain/`
269
+ - ✅ Use GORM, sqlx, or standard `database/sql` — never leak DB types into domain
270
+ - ✅ Convert ORM models to domain entities inside the repository
271
+
272
+ ---
273
+
274
+ ## 6. Interfaces Layer — HTTP Handlers
275
+
276
+ ```go
277
+ // handler.go — HTTP handler that translates HTTP to application calls.
278
+
279
+ type Handler struct {
280
+ svc application.Service
281
+ logger *slog.Logger
282
+ }
283
+
284
+ func NewHandler(svc application.Service, logger *slog.Logger) *Handler {
285
+ return &Handler{svc: svc, logger: logger}
286
+ }
287
+
288
+ func (h *Handler) Execute(w http.ResponseWriter, r *http.Request) {
289
+ var req ExecuteRequest
290
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
291
+ http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
292
+ return
293
+ }
294
+
295
+ entity, err := h.svc.Execute(r.Context(), req.ToCommand())
296
+ if err != nil {
297
+ var de *DomainError
298
+ if errors.As(err, &de) {
299
+ http.Error(w, fmt.Sprintf(`{"error":"%s"}`, de.Message), errorHTTPStatus(de.Code))
300
+ return
301
+ }
302
+ h.logger.ErrorContext(r.Context(), "execute failed", "error", err)
303
+ http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
304
+ return
305
+ }
306
+
307
+ w.Header().Set("Content-Type", "application/json")
308
+ json.NewEncoder(w).Encode(entityToResponse(entity))
309
+ }
310
+ ```
311
+
312
+ ### Rules
313
+ - ✅ Handlers translate HTTP to application calls — no business logic
314
+ - ✅ Always use structured error responses (JSON)
315
+ - ✅ Map domain errors to appropriate HTTP status codes
316
+ - ✅ Use standard `net/http` or chi/gin router, not framework-specific types in handlers
317
+
318
+ ---
319
+
320
+ ## 7. Configuration — Environment-Based
321
+
322
+ ```go
323
+ type Config struct {
324
+ Port int `envconfig:"PORT" default:"8080"`
325
+ DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
326
+ LogLevel slog.Level `envconfig:"LOG_LEVEL" default:"info"`
327
+ APIKey string `envconfig:"API_KEY"` // Loaded from env, never from config file
328
+ }
329
+
330
+ func LoadConfig() (*Config, error) {
331
+ var cfg Config
332
+ if err := envconfig.Process("APP", &cfg); err != nil {
333
+ return nil, fmt.Errorf("load config: %w", err)
334
+ }
335
+ return &cfg, nil
336
+ }
337
+ ```
338
+
339
+ ### Rules
340
+ - ✅ Use `envconfig` or `viper` for configuration
341
+ - ✅ CLI flags > Environment > Config file > Defaults
342
+ - ✅ Secrets loaded ONLY from environment variables
343
+ - ✅ Validate on startup — fail fast
344
+
345
+ ---
346
+
347
+ ## 8. Testing Patterns
348
+
349
+ ### Unit Tests
350
+
351
+ ```go
352
+ func TestEntity_Transition(t *testing.T) {
353
+ e := NewEntity(primitive.NewObjectID())
354
+
355
+ // Initial state
356
+ assert.Equal(t, StatusPending, e.Status())
357
+
358
+ // Valid transition
359
+ err := e.transition(StatusActive)
360
+ assert.NoError(t, err)
361
+ assert.Equal(t, StatusActive, e.Status())
362
+
363
+ // Invalid transition
364
+ err = e.transition(StatusPending) // Can't go back
365
+ assert.Error(t, err)
366
+ var de *DomainError
367
+ assert.True(t, errors.As(err, &de))
368
+ assert.Equal(t, ErrInvalidTransition, de.Code)
369
+ }
370
+ ```
371
+
372
+ ### Repository Tests with Testcontainers
373
+
374
+ ```go
375
+ func TestGormRepository(t *testing.T) {
376
+ // Spin up test PostgreSQL container
377
+ testDB, err := setupTestDatabase(t)
378
+ require.NoError(t, err)
379
+
380
+ repo := NewGormRepository(testDB, slog.Default())
381
+
382
+ entity := NewEntity(primitive.NewObjectID())
383
+ err = repo.Save(context.Background(), entity)
384
+ require.NoError(t, err)
385
+
386
+ found, err := repo.FindByID(context.Background(), entity.ID())
387
+ require.NoError(t, err)
388
+ assert.Equal(t, entity.Status(), found.Status())
389
+ }
390
+ ```
391
+
392
+ ---
393
+
394
+ ## 9. Anti-Patterns — NEVER DO
395
+
396
+ ```go
397
+ // ❌ Panic for error handling
398
+ if err != nil { panic(err) } // BAD
399
+
400
+ // ❌ Stringly-typed errors
401
+ errors.New("something went wrong") // BAD — use typed DomainError
402
+
403
+ // ❌ Global state
404
+ var db *gorm.DB // BAD — use dependency injection
405
+
406
+ // ❌ Business logic in handlers
407
+ func Handler(w http.ResponseWriter, r *http.Request) {
408
+ // complex business logic here // BAD — belongs in domain/application
409
+ }
410
+
411
+ // ❌ Leaking DB models to domain
412
+ type User struct { gorm.Model } // BAD — domain should not know about GORM
413
+
414
+ // ❌ Ignoring errors
415
+ json.NewEncoder(w).Encode(data) // BAD — always handle errors
416
+
417
+ // ❌ Background goroutines in handlers
418
+ go doSomething() // BAD — use proper worker pools with lifecycle management
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 10. Go Project-Level Structure
424
+
425
+ ```
426
+ cmd/
427
+ server/
428
+ main.go // Entry point, DI setup
429
+ internal/
430
+ module/ // One directory per bounded context
431
+ domain/
432
+ application/
433
+ infrastructure/
434
+ repository/
435
+ interfaces/
436
+ pkg/
437
+ framework/ // Shared infrastructure
438
+ database.go
439
+ logger.go
440
+ router.go
441
+ docker-compose.yml
442
+ Dockerfile
443
+ go.mod
444
+ ```
445
+
446
+ ---
447
+
448
+ *Version: 1.0.0*
449
+ *Last updated: 2026-07-03*
450
+ *Source: Guardian DDD patterns + Clean Architecture + context7 (/wesionaryteam/go_clean_architecture, /golang/go)*