@anhth2/spec-driven-dev-plugin 0.5.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.
- package/ARCHITECTURE.md +243 -0
- package/bin/build.js +230 -0
- package/bin/index.js +311 -0
- package/commands/debug.md +374 -0
- package/commands/debug.tmpl +77 -0
- package/commands/define-product.md +451 -0
- package/commands/define-product.tmpl +154 -0
- package/commands/fix-bug.md +379 -0
- package/commands/fix-bug.tmpl +82 -0
- package/commands/generate-bdd.md +591 -0
- package/commands/generate-bdd.tmpl +294 -0
- package/commands/generate-code.md +395 -0
- package/commands/generate-code.tmpl +98 -0
- package/commands/generate-prd.md +488 -0
- package/commands/generate-prd.tmpl +191 -0
- package/commands/generate-tech-docs.md +362 -0
- package/commands/generate-tech-docs.tmpl +65 -0
- package/commands/generate-tests.md +377 -0
- package/commands/generate-tests.tmpl +80 -0
- package/commands/refine-prd.md +408 -0
- package/commands/refine-prd.tmpl +111 -0
- package/commands/review-code.md +354 -0
- package/commands/review-code.tmpl +57 -0
- package/commands/review-context.md +646 -0
- package/commands/review-context.tmpl +349 -0
- package/commands/review-tech-docs.md +518 -0
- package/commands/review-tech-docs.tmpl +221 -0
- package/commands/run-tests.md +343 -0
- package/commands/run-tests.tmpl +46 -0
- package/commands/setup-ai-first.md +278 -0
- package/commands/setup-ai-first.tmpl +197 -0
- package/commands/smoke-test.md +366 -0
- package/commands/smoke-test.tmpl +69 -0
- package/commands/validate-traces.md +529 -0
- package/commands/validate-traces.tmpl +232 -0
- package/core/FRAMEWORK_VERSION +1 -0
- package/core/commands/debug.md +374 -0
- package/core/commands/define-product.md +451 -0
- package/core/commands/fix-bug.md +379 -0
- package/core/commands/generate-bdd.md +591 -0
- package/core/commands/generate-code.md +395 -0
- package/core/commands/generate-prd.md +488 -0
- package/core/commands/generate-tech-docs.md +362 -0
- package/core/commands/generate-tests.md +377 -0
- package/core/commands/refine-prd.md +408 -0
- package/core/commands/review-code.md +354 -0
- package/core/commands/review-context.md +646 -0
- package/core/commands/review-tech-docs.md +518 -0
- package/core/commands/run-tests.md +343 -0
- package/core/commands/setup-ai-first.md +278 -0
- package/core/commands/smoke-test.md +366 -0
- package/core/commands/validate-traces.md +529 -0
- package/core/hooks/data-guard.js +141 -0
- package/core/hooks/settings.json +18 -0
- package/core/modules/angular/architecture-snippets/component-patterns.md +187 -0
- package/core/modules/angular/module.yaml +6 -0
- package/core/modules/angular/stack-profile.yaml +38 -0
- package/core/modules/context-engineering/architecture-snippets/context-design.md +119 -0
- package/core/modules/context-engineering/module.yaml +9 -0
- package/core/modules/context-engineering/stack-profile.yaml +61 -0
- package/core/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
- package/core/modules/dotnet/module.yaml +6 -0
- package/core/modules/dotnet/stack-profile.yaml +50 -0
- package/core/modules/golang/architecture-snippets/domain-layout.md +283 -0
- package/core/modules/golang/module.yaml +6 -0
- package/core/modules/golang/stack-profile.yaml +40 -0
- package/core/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
- package/core/modules/java-spring/module.yaml +15 -0
- package/core/modules/java-spring/stack-profile.yaml +28 -0
- package/core/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
- package/core/modules/nextjs/module.yaml +14 -0
- package/core/modules/nextjs/stack-profile.yaml +74 -0
- package/core/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
- package/core/modules/php-laravel/module.yaml +15 -0
- package/core/modules/php-laravel/stack-profile.yaml +56 -0
- package/core/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
- package/core/modules/react/module.yaml +14 -0
- package/core/modules/react/stack-profile.yaml +63 -0
- package/core/rules/data-protection.md +80 -0
- package/core/rules/workflow.md +44 -0
- package/core/skills/code/SKILL.md +526 -0
- package/core/skills/debug/SKILL.md +584 -0
- package/core/skills/discovery/SKILL.md +363 -0
- package/core/skills/prd/SKILL.md +456 -0
- package/core/skills/setup-ai-first/SKILL.md +160 -0
- package/core/skills/spec/SKILL.md +361 -0
- package/core/skills/test/SKILL.md +862 -0
- package/core/steps/context-loader.md +163 -0
- package/core/steps/gate.md +81 -0
- package/core/steps/report-footer.md +53 -0
- package/core/steps/spawn-agent.md +123 -0
- package/core/templates/architecture.template.md +113 -0
- package/core/templates/feature.template +259 -0
- package/core/templates/platform-guide.template.md +145 -0
- package/core/templates/prd.template.md +312 -0
- package/core/templates/product-definition.template.md +168 -0
- package/core/templates/project-context.yaml +78 -0
- package/hooks/data-guard.js +141 -0
- package/hooks/settings.json +18 -0
- package/modules/angular/architecture-snippets/component-patterns.md +187 -0
- package/modules/angular/module.yaml +6 -0
- package/modules/angular/stack-profile.yaml +38 -0
- package/modules/context-engineering/architecture-snippets/context-design.md +119 -0
- package/modules/context-engineering/module.yaml +9 -0
- package/modules/context-engineering/stack-profile.yaml +61 -0
- package/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
- package/modules/dotnet/module.yaml +6 -0
- package/modules/dotnet/stack-profile.yaml +50 -0
- package/modules/golang/architecture-snippets/domain-layout.md +283 -0
- package/modules/golang/module.yaml +6 -0
- package/modules/golang/stack-profile.yaml +40 -0
- package/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
- package/modules/java-spring/module.yaml +15 -0
- package/modules/java-spring/stack-profile.yaml +28 -0
- package/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
- package/modules/nextjs/module.yaml +14 -0
- package/modules/nextjs/stack-profile.yaml +74 -0
- package/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
- package/modules/php-laravel/module.yaml +15 -0
- package/modules/php-laravel/stack-profile.yaml +56 -0
- package/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
- package/modules/react/module.yaml +14 -0
- package/modules/react/stack-profile.yaml +63 -0
- package/package.json +42 -0
- package/rules/data-protection.md +80 -0
- package/rules/workflow.md +44 -0
- package/scripts/init.sh +49 -0
- package/scripts/upgrade.sh +94 -0
- package/skills/code/SKILL.md +526 -0
- package/skills/code/SKILL.tmpl +176 -0
- package/skills/debug/SKILL.md +584 -0
- package/skills/debug/SKILL.tmpl +262 -0
- package/skills/discovery/SKILL.md +363 -0
- package/skills/discovery/SKILL.tmpl +147 -0
- package/skills/prd/SKILL.md +456 -0
- package/skills/prd/SKILL.tmpl +188 -0
- package/skills/setup-ai-first/SKILL.md +160 -0
- package/skills/setup-ai-first/SKILL.tmpl +107 -0
- package/skills/spec/SKILL.md +361 -0
- package/skills/spec/SKILL.tmpl +174 -0
- package/skills/test/SKILL.md +862 -0
- package/skills/test/SKILL.tmpl +296 -0
- package/steps/context-loader.md +163 -0
- package/steps/gate.md +81 -0
- package/steps/report-footer.md +53 -0
- package/steps/spawn-agent.md +123 -0
- package/templates/architecture.template.md +113 -0
- package/templates/feature.template +259 -0
- package/templates/platform-guide.template.md +145 -0
- package/templates/prd.template.md +312 -0
- package/templates/product-definition.template.md +168 -0
- package/templates/project-context.yaml +78 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Go — Domain-Driven Layout Code Patterns
|
|
2
|
+
|
|
3
|
+
## Domain Model
|
|
4
|
+
|
|
5
|
+
```go
|
|
6
|
+
// internal/domain/order.go
|
|
7
|
+
package domain
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"errors"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// OrderStatus represents the lifecycle state of an order.
|
|
15
|
+
type OrderStatus string
|
|
16
|
+
|
|
17
|
+
const (
|
|
18
|
+
OrderStatusPending OrderStatus = "PENDING"
|
|
19
|
+
OrderStatusConfirmed OrderStatus = "CONFIRMED"
|
|
20
|
+
OrderStatusShipped OrderStatus = "SHIPPED"
|
|
21
|
+
OrderStatusCancelled OrderStatus = "CANCELLED"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// Order is the aggregate root for the order domain.
|
|
25
|
+
type Order struct {
|
|
26
|
+
ID int64
|
|
27
|
+
CustomerID int64
|
|
28
|
+
Status OrderStatus
|
|
29
|
+
Items []OrderItem
|
|
30
|
+
ShippingAddress string
|
|
31
|
+
CreatedAt time.Time
|
|
32
|
+
UpdatedAt time.Time
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// OrderItem represents a single line item within an order.
|
|
36
|
+
type OrderItem struct {
|
|
37
|
+
ProductID int64
|
|
38
|
+
Quantity int
|
|
39
|
+
UnitPrice float64
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// TotalAmount calculates the total order value.
|
|
43
|
+
func (o *Order) TotalAmount() float64 {
|
|
44
|
+
var total float64
|
|
45
|
+
for _, item := range o.Items {
|
|
46
|
+
total += float64(item.Quantity) * item.UnitPrice
|
|
47
|
+
}
|
|
48
|
+
return total
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Cancel transitions the order to CANCELLED status.
|
|
52
|
+
func (o *Order) Cancel() error {
|
|
53
|
+
if o.Status == OrderStatusShipped {
|
|
54
|
+
return errors.New("cannot cancel an order that has already been shipped")
|
|
55
|
+
}
|
|
56
|
+
o.Status = OrderStatusCancelled
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Repository Interface (defined in domain)
|
|
62
|
+
|
|
63
|
+
```go
|
|
64
|
+
// internal/domain/order_repository.go
|
|
65
|
+
package domain
|
|
66
|
+
|
|
67
|
+
import "context"
|
|
68
|
+
|
|
69
|
+
// OrderRepository defines persistence operations for orders.
|
|
70
|
+
// Implementations live in internal/repo/.
|
|
71
|
+
type OrderRepository interface {
|
|
72
|
+
GetByID(ctx context.Context, id int64) (*Order, error)
|
|
73
|
+
GetByCustomerID(ctx context.Context, customerID int64) ([]*Order, error)
|
|
74
|
+
Create(ctx context.Context, order *Order) error
|
|
75
|
+
Update(ctx context.Context, order *Order) error
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Use Case (business logic)
|
|
80
|
+
|
|
81
|
+
```go
|
|
82
|
+
// internal/usecase/order_usecase.go
|
|
83
|
+
// @trace.implements=ORDER-UC1
|
|
84
|
+
package usecase
|
|
85
|
+
|
|
86
|
+
import (
|
|
87
|
+
"context"
|
|
88
|
+
"fmt"
|
|
89
|
+
"myapp/internal/domain"
|
|
90
|
+
"time"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// OrderUseCase handles all order-related business operations.
|
|
94
|
+
type OrderUseCase struct {
|
|
95
|
+
repo domain.OrderRepository
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// NewOrderUseCase creates a new OrderUseCase with required dependencies.
|
|
99
|
+
func NewOrderUseCase(repo domain.OrderRepository) *OrderUseCase {
|
|
100
|
+
return &OrderUseCase{repo: repo}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// CreateOrderRequest holds input data for creating an order.
|
|
104
|
+
type CreateOrderRequest struct {
|
|
105
|
+
CustomerID int64
|
|
106
|
+
Items []domain.OrderItem
|
|
107
|
+
ShippingAddress string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// CreateOrder creates a new order for a customer.
|
|
111
|
+
// @trace.implements=ORDER-UC1-SC1
|
|
112
|
+
func (uc *OrderUseCase) CreateOrder(ctx context.Context, req CreateOrderRequest) (*domain.Order, error) {
|
|
113
|
+
if len(req.Items) == 0 {
|
|
114
|
+
return nil, fmt.Errorf("order must contain at least one item")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
order := &domain.Order{
|
|
118
|
+
CustomerID: req.CustomerID,
|
|
119
|
+
Status: domain.OrderStatusPending,
|
|
120
|
+
Items: req.Items,
|
|
121
|
+
ShippingAddress: req.ShippingAddress,
|
|
122
|
+
CreatedAt: time.Now(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if err := uc.repo.Create(ctx, order); err != nil {
|
|
126
|
+
return nil, fmt.Errorf("create order: %w", err)
|
|
127
|
+
}
|
|
128
|
+
return order, nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// GetOrder retrieves an order by ID.
|
|
132
|
+
// @trace.implements=ORDER-UC1-SC2
|
|
133
|
+
func (uc *OrderUseCase) GetOrder(ctx context.Context, id int64) (*domain.Order, error) {
|
|
134
|
+
order, err := uc.repo.GetByID(ctx, id)
|
|
135
|
+
if err != nil {
|
|
136
|
+
return nil, fmt.Errorf("get order %d: %w", id, err)
|
|
137
|
+
}
|
|
138
|
+
if order == nil {
|
|
139
|
+
return nil, domain.ErrNotFound
|
|
140
|
+
}
|
|
141
|
+
return order, nil
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## HTTP Handler (Gin)
|
|
146
|
+
|
|
147
|
+
```go
|
|
148
|
+
// internal/handler/order_handler.go
|
|
149
|
+
// @trace.implements=ORDER-UC1
|
|
150
|
+
package handler
|
|
151
|
+
|
|
152
|
+
import (
|
|
153
|
+
"errors"
|
|
154
|
+
"net/http"
|
|
155
|
+
"strconv"
|
|
156
|
+
|
|
157
|
+
"github.com/gin-gonic/gin"
|
|
158
|
+
"myapp/internal/domain"
|
|
159
|
+
"myapp/internal/usecase"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// OrderHandler handles HTTP requests for order operations.
|
|
163
|
+
type OrderHandler struct {
|
|
164
|
+
uc *usecase.OrderUseCase
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// NewOrderHandler creates a new OrderHandler.
|
|
168
|
+
func NewOrderHandler(uc *usecase.OrderUseCase) *OrderHandler {
|
|
169
|
+
return &OrderHandler{uc: uc}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// RegisterRoutes registers all order endpoints on the given router group.
|
|
173
|
+
func (h *OrderHandler) RegisterRoutes(r *gin.RouterGroup) {
|
|
174
|
+
r.POST("/orders", h.CreateOrder) // @trace.implements=ORDER-UC1-SC1
|
|
175
|
+
r.GET("/orders/:id", h.GetOrder) // @trace.implements=ORDER-UC1-SC2
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// CreateOrder handles POST /v1/orders.
|
|
179
|
+
func (h *OrderHandler) CreateOrder(c *gin.Context) {
|
|
180
|
+
var req usecase.CreateOrderRequest
|
|
181
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
182
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
order, err := h.uc.CreateOrder(c.Request.Context(), req)
|
|
186
|
+
if err != nil {
|
|
187
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
c.JSON(http.StatusCreated, order)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// GetOrder handles GET /v1/orders/:id.
|
|
194
|
+
func (h *OrderHandler) GetOrder(c *gin.Context) {
|
|
195
|
+
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
196
|
+
if err != nil {
|
|
197
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid order id"})
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
order, err := h.uc.GetOrder(c.Request.Context(), id)
|
|
201
|
+
if errors.Is(err, domain.ErrNotFound) {
|
|
202
|
+
c.JSON(http.StatusNotFound, gin.H{"error": "order not found"})
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if err != nil {
|
|
206
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
c.JSON(http.StatusOK, order)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Table-Driven Test Pattern
|
|
214
|
+
|
|
215
|
+
```go
|
|
216
|
+
// internal/usecase/order_usecase_test.go
|
|
217
|
+
// @trace.verifies=ORDER-UC1
|
|
218
|
+
// @trace.test_type=unit
|
|
219
|
+
package usecase_test
|
|
220
|
+
|
|
221
|
+
import (
|
|
222
|
+
"context"
|
|
223
|
+
"testing"
|
|
224
|
+
|
|
225
|
+
"github.com/stretchr/testify/assert"
|
|
226
|
+
"github.com/stretchr/testify/mock"
|
|
227
|
+
"myapp/internal/domain"
|
|
228
|
+
"myapp/internal/usecase"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
func TestOrderUseCase_CreateOrder(t *testing.T) {
|
|
232
|
+
tests := []struct {
|
|
233
|
+
name string
|
|
234
|
+
request usecase.CreateOrderRequest
|
|
235
|
+
setupMock func(*MockOrderRepository)
|
|
236
|
+
expectError bool
|
|
237
|
+
expectOrder bool
|
|
238
|
+
}{
|
|
239
|
+
{
|
|
240
|
+
name: "valid order with items should create successfully",
|
|
241
|
+
request: usecase.CreateOrderRequest{
|
|
242
|
+
CustomerID: 1,
|
|
243
|
+
Items: []domain.OrderItem{{ProductID: 10, Quantity: 2, UnitPrice: 99.9}},
|
|
244
|
+
ShippingAddress: "123 Main St",
|
|
245
|
+
},
|
|
246
|
+
setupMock: func(m *MockOrderRepository) {
|
|
247
|
+
m.On("Create", mock.Anything, mock.AnythingOfType("*domain.Order")).Return(nil)
|
|
248
|
+
},
|
|
249
|
+
expectError: false,
|
|
250
|
+
expectOrder: true,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "empty items should return validation error",
|
|
254
|
+
request: usecase.CreateOrderRequest{
|
|
255
|
+
CustomerID: 1,
|
|
256
|
+
Items: []domain.OrderItem{},
|
|
257
|
+
},
|
|
258
|
+
setupMock: func(m *MockOrderRepository) {},
|
|
259
|
+
expectError: true,
|
|
260
|
+
expectOrder: false,
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for _, tt := range tests {
|
|
265
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
266
|
+
mockRepo := new(MockOrderRepository)
|
|
267
|
+
tt.setupMock(mockRepo)
|
|
268
|
+
uc := usecase.NewOrderUseCase(mockRepo)
|
|
269
|
+
|
|
270
|
+
order, err := uc.CreateOrder(context.Background(), tt.request)
|
|
271
|
+
|
|
272
|
+
if tt.expectError {
|
|
273
|
+
assert.Error(t, err)
|
|
274
|
+
assert.Nil(t, order)
|
|
275
|
+
} else {
|
|
276
|
+
assert.NoError(t, err)
|
|
277
|
+
assert.NotNil(t, order)
|
|
278
|
+
}
|
|
279
|
+
mockRepo.AssertExpectations(t)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
build:
|
|
2
|
+
compile: "go build ./..."
|
|
3
|
+
test: "go test ./..."
|
|
4
|
+
run: "go run ./cmd/{service-name}"
|
|
5
|
+
|
|
6
|
+
architecture:
|
|
7
|
+
style: "Domain-driven layout"
|
|
8
|
+
directory_structure:
|
|
9
|
+
cmd/: "Main entry points, one per binary (e.g., cmd/api/main.go)"
|
|
10
|
+
internal/domain/: "Domain entities, interfaces, and business rules"
|
|
11
|
+
internal/repo/: "Repository implementations (DB, cache)"
|
|
12
|
+
internal/handler/: "HTTP handlers (Gin/Echo) — thin, delegate to use cases"
|
|
13
|
+
internal/usecase/: "Use case implementations (business logic)"
|
|
14
|
+
pkg/: "Reusable packages safe for external use"
|
|
15
|
+
config/: "Configuration loading (env vars, YAML)"
|
|
16
|
+
key_rules:
|
|
17
|
+
- "Define interfaces in the consumer package (dependency inversion)"
|
|
18
|
+
- "internal/ packages cannot be imported outside the module"
|
|
19
|
+
- "Handlers must not contain business logic — only parse, call use case, respond"
|
|
20
|
+
- "Use cases own business rules and call repository interfaces"
|
|
21
|
+
- "Repositories implement persistence — never call use cases"
|
|
22
|
+
|
|
23
|
+
coding_standards:
|
|
24
|
+
naming:
|
|
25
|
+
packages: "lowercase single word (e.g., order, handler, repo)"
|
|
26
|
+
interfaces: "Describe behavior, no 'I' prefix (e.g., OrderRepository, not IOrderRepository)"
|
|
27
|
+
structs: "PascalCase for exported, camelCase for unexported"
|
|
28
|
+
methods: "PascalCase for exported, camelCase for unexported"
|
|
29
|
+
patterns:
|
|
30
|
+
error_handling: "Return error as last return value; use errors.Is/As; wrap with fmt.Errorf(\"...: %w\", err)"
|
|
31
|
+
interfaces: "Define small interfaces at point of use; prefer 1-2 method interfaces"
|
|
32
|
+
constructors: "NewXxx(deps...) *Xxx — always use constructor functions"
|
|
33
|
+
|
|
34
|
+
testing:
|
|
35
|
+
framework: "Standard library testing package + testify"
|
|
36
|
+
patterns:
|
|
37
|
+
- "Table-driven tests for all cases"
|
|
38
|
+
- "Use testify/assert and testify/mock"
|
|
39
|
+
- "Test file: {file}_test.go in same package (white-box) or {pkg}_test package (black-box)"
|
|
40
|
+
- "Integration tests in internal/{pkg}/integration_test.go with build tag //go:build integration"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Java Spring Boot — Layered Architecture Code Patterns
|
|
2
|
+
|
|
3
|
+
## Controller Pattern
|
|
4
|
+
|
|
5
|
+
```java
|
|
6
|
+
@RestController
|
|
7
|
+
@RequestMapping("/v1/orders")
|
|
8
|
+
@RequiredArgsConstructor
|
|
9
|
+
// @trace.implements=ORDER-UC1-SC1
|
|
10
|
+
// @trace.source=specs/bdd/order/ORDER-UC1-create-order.feature
|
|
11
|
+
public class OrderController {
|
|
12
|
+
|
|
13
|
+
private final OrderFacade orderFacade;
|
|
14
|
+
|
|
15
|
+
@PostMapping
|
|
16
|
+
public ResponseEntity<ApiResponse<OrderResponse>> createOrder(
|
|
17
|
+
@Valid @RequestBody CreateOrderRequest request) {
|
|
18
|
+
OrderResponse result = orderFacade.createOrder(request);
|
|
19
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
20
|
+
.body(ApiResponse.success(result));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@GetMapping("/{id}")
|
|
24
|
+
// @trace.implements=ORDER-UC1-SC2
|
|
25
|
+
public ResponseEntity<ApiResponse<OrderResponse>> getOrder(@PathVariable Long id) {
|
|
26
|
+
OrderResponse result = orderFacade.getOrder(id);
|
|
27
|
+
return ResponseEntity.ok(ApiResponse.success(result));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Service Interface Pattern
|
|
33
|
+
|
|
34
|
+
```java
|
|
35
|
+
public interface OrderService {
|
|
36
|
+
OrderResponse createOrder(CreateOrderRequest request);
|
|
37
|
+
OrderResponse getOrderById(Long id);
|
|
38
|
+
void cancelOrder(Long id);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Service Implementation Pattern
|
|
43
|
+
|
|
44
|
+
```java
|
|
45
|
+
@Service
|
|
46
|
+
@RequiredArgsConstructor
|
|
47
|
+
@Transactional(readOnly = true)
|
|
48
|
+
public class OrderServiceImpl implements OrderService {
|
|
49
|
+
|
|
50
|
+
private final OrderRepository orderRepository;
|
|
51
|
+
private final OrderMapper orderMapper;
|
|
52
|
+
|
|
53
|
+
@Override
|
|
54
|
+
@Transactional
|
|
55
|
+
public OrderResponse createOrder(CreateOrderRequest request) {
|
|
56
|
+
Order order = orderMapper.toEntity(request);
|
|
57
|
+
order.setStatus(OrderStatus.PENDING);
|
|
58
|
+
Order saved = orderRepository.save(order);
|
|
59
|
+
return orderMapper.toResponse(saved);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Override
|
|
63
|
+
public OrderResponse getOrderById(Long id) {
|
|
64
|
+
Order order = orderRepository.findById(id)
|
|
65
|
+
.orElseThrow(() -> new ResourceNotFoundException("Order", id));
|
|
66
|
+
return orderMapper.toResponse(order);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Override
|
|
70
|
+
@Transactional
|
|
71
|
+
public void cancelOrder(Long id) {
|
|
72
|
+
Order order = orderRepository.findById(id)
|
|
73
|
+
.orElseThrow(() -> new ResourceNotFoundException("Order", id));
|
|
74
|
+
order.setStatus(OrderStatus.CANCELLED);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Repository Interface Pattern
|
|
80
|
+
|
|
81
|
+
```java
|
|
82
|
+
@Repository
|
|
83
|
+
public interface OrderRepository extends JpaRepository<Order, Long> {
|
|
84
|
+
|
|
85
|
+
List<Order> findByCustomerIdAndStatus(Long customerId, OrderStatus status);
|
|
86
|
+
|
|
87
|
+
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
|
|
88
|
+
Optional<Order> findByIdWithItems(@Param("id") Long id);
|
|
89
|
+
|
|
90
|
+
Page<Order> findByCreatedAtBetween(
|
|
91
|
+
LocalDateTime from, LocalDateTime to, Pageable pageable);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## DTO Pattern
|
|
96
|
+
|
|
97
|
+
```java
|
|
98
|
+
// Request DTO
|
|
99
|
+
@Data
|
|
100
|
+
@NoArgsConstructor
|
|
101
|
+
@AllArgsConstructor
|
|
102
|
+
public class CreateOrderRequest {
|
|
103
|
+
@NotNull(message = "customerId is required")
|
|
104
|
+
private Long customerId;
|
|
105
|
+
|
|
106
|
+
@NotEmpty(message = "items must not be empty")
|
|
107
|
+
private List<OrderItemRequest> items;
|
|
108
|
+
|
|
109
|
+
@NotBlank(message = "shippingAddress is required")
|
|
110
|
+
private String shippingAddress;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Response DTO
|
|
114
|
+
@Data
|
|
115
|
+
@Builder
|
|
116
|
+
@NoArgsConstructor
|
|
117
|
+
@AllArgsConstructor
|
|
118
|
+
public class OrderResponse {
|
|
119
|
+
private Long id;
|
|
120
|
+
private Long customerId;
|
|
121
|
+
private OrderStatus status;
|
|
122
|
+
private List<OrderItemResponse> items;
|
|
123
|
+
private BigDecimal totalAmount;
|
|
124
|
+
private LocalDateTime createdAt;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## MapStruct Mapper Pattern
|
|
129
|
+
|
|
130
|
+
```java
|
|
131
|
+
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
|
|
132
|
+
public interface OrderMapper {
|
|
133
|
+
|
|
134
|
+
@Mapping(target = "id", ignore = true)
|
|
135
|
+
@Mapping(target = "status", ignore = true)
|
|
136
|
+
@Mapping(target = "createdAt", ignore = true)
|
|
137
|
+
Order toEntity(CreateOrderRequest request);
|
|
138
|
+
|
|
139
|
+
OrderResponse toResponse(Order order);
|
|
140
|
+
|
|
141
|
+
List<OrderResponse> toResponseList(List<Order> orders);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## ApiResponse Wrapper Pattern
|
|
146
|
+
|
|
147
|
+
```java
|
|
148
|
+
@Data
|
|
149
|
+
@Builder
|
|
150
|
+
@NoArgsConstructor
|
|
151
|
+
@AllArgsConstructor
|
|
152
|
+
public class ApiResponse<T> {
|
|
153
|
+
private boolean success;
|
|
154
|
+
private T data;
|
|
155
|
+
private String message;
|
|
156
|
+
private String errorCode;
|
|
157
|
+
private LocalDateTime timestamp;
|
|
158
|
+
|
|
159
|
+
public static <T> ApiResponse<T> success(T data) {
|
|
160
|
+
return ApiResponse.<T>builder()
|
|
161
|
+
.success(true)
|
|
162
|
+
.data(data)
|
|
163
|
+
.timestamp(LocalDateTime.now())
|
|
164
|
+
.build();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public static <T> ApiResponse<T> error(String errorCode, String message) {
|
|
168
|
+
return ApiResponse.<T>builder()
|
|
169
|
+
.success(false)
|
|
170
|
+
.errorCode(errorCode)
|
|
171
|
+
.message(message)
|
|
172
|
+
.timestamp(LocalDateTime.now())
|
|
173
|
+
.build();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Facade Pattern (optional orchestration layer)
|
|
179
|
+
|
|
180
|
+
```java
|
|
181
|
+
@Service
|
|
182
|
+
@RequiredArgsConstructor
|
|
183
|
+
public class OrderFacade {
|
|
184
|
+
|
|
185
|
+
private final OrderService orderService;
|
|
186
|
+
private final InventoryService inventoryService;
|
|
187
|
+
private final NotificationService notificationService;
|
|
188
|
+
|
|
189
|
+
public OrderResponse createOrder(CreateOrderRequest request) {
|
|
190
|
+
// 1. Validate inventory
|
|
191
|
+
inventoryService.validateAvailability(request.getItems());
|
|
192
|
+
// 2. Create order
|
|
193
|
+
OrderResponse order = orderService.createOrder(request);
|
|
194
|
+
// 3. Reserve inventory
|
|
195
|
+
inventoryService.reserveItems(order.getId(), request.getItems());
|
|
196
|
+
// 4. Send confirmation
|
|
197
|
+
notificationService.sendOrderConfirmation(order);
|
|
198
|
+
return order;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: "Java Spring Boot"
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: "Spring Boot 3.x with layered architecture"
|
|
4
|
+
language: "Java"
|
|
5
|
+
framework: "Spring Boot"
|
|
6
|
+
stack_type: "backend"
|
|
7
|
+
default_layer_order:
|
|
8
|
+
- DTO
|
|
9
|
+
- Entity/Model
|
|
10
|
+
- Repository
|
|
11
|
+
- Service interface
|
|
12
|
+
- Service impl
|
|
13
|
+
- Facade (optional)
|
|
14
|
+
- Controller
|
|
15
|
+
test_framework: "JUnit 5 + Mockito"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
build:
|
|
2
|
+
compile: "mvn clean install -DskipTests"
|
|
3
|
+
test: "mvn test"
|
|
4
|
+
run: "mvn spring-boot:run"
|
|
5
|
+
|
|
6
|
+
architecture:
|
|
7
|
+
style: "Layered (Controller → Facade → Service → Repository)"
|
|
8
|
+
key_rules:
|
|
9
|
+
- "Controllers must not contain business logic"
|
|
10
|
+
- "Services own transaction boundaries (@Transactional)"
|
|
11
|
+
- "Repositories use Spring Data JPA"
|
|
12
|
+
- "Facades orchestrate multiple services (optional layer)"
|
|
13
|
+
|
|
14
|
+
coding_standards:
|
|
15
|
+
naming:
|
|
16
|
+
classes: "PascalCase"
|
|
17
|
+
methods: "camelCase"
|
|
18
|
+
packages: "lowercase.snake_case"
|
|
19
|
+
patterns:
|
|
20
|
+
response_wrapper: "ApiResponse<T>"
|
|
21
|
+
mapping: "MapStruct"
|
|
22
|
+
exception_base: "ResourceNotFoundException"
|
|
23
|
+
|
|
24
|
+
trace_tags:
|
|
25
|
+
implements: "@trace.implements={UC-ID}-SC{N}"
|
|
26
|
+
source: "@trace.source=specs/bdd/{domain}/{UC-ID}.feature"
|
|
27
|
+
verifies: "@trace.verifies={UC-ID}"
|
|
28
|
+
test_type: "@trace.test_type=unit|integration"
|