@corbat-tech/coding-standards-mcp 1.0.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/LICENSE +21 -0
- package/README.md +371 -0
- package/assets/demo.gif +0 -0
- package/dist/agent.d.ts +53 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +629 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +651 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +105 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/profiles.d.ts +39 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +526 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompts-legacy.d.ts +25 -0
- package/dist/prompts-legacy.d.ts.map +1 -0
- package/dist/prompts-legacy.js +600 -0
- package/dist/prompts-legacy.js.map +1 -0
- package/dist/prompts-v2.d.ts +30 -0
- package/dist/prompts-v2.d.ts.map +1 -0
- package/dist/prompts-v2.js +310 -0
- package/dist/prompts-v2.js.map +1 -0
- package/dist/prompts.d.ts +30 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +310 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +18 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +95 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools-legacy.d.ts +196 -0
- package/dist/tools-legacy.d.ts.map +1 -0
- package/dist/tools-legacy.js +1230 -0
- package/dist/tools-legacy.js.map +1 -0
- package/dist/tools-v2.d.ts +92 -0
- package/dist/tools-v2.d.ts.map +1 -0
- package/dist/tools-v2.js +410 -0
- package/dist/tools-v2.js.map +1 -0
- package/dist/tools.d.ts +92 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +410 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +3054 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +515 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/retry.d.ts +44 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +74 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +79 -0
- package/profiles/README.md +199 -0
- package/profiles/custom/.gitkeep +2 -0
- package/profiles/templates/_template.yaml +159 -0
- package/profiles/templates/angular.yaml +494 -0
- package/profiles/templates/java-spring-backend.yaml +512 -0
- package/profiles/templates/minimal.yaml +102 -0
- package/profiles/templates/nodejs.yaml +338 -0
- package/profiles/templates/python.yaml +340 -0
- package/profiles/templates/react.yaml +331 -0
- package/profiles/templates/vue.yaml +598 -0
- package/standards/architecture/ddd.md +173 -0
- package/standards/architecture/hexagonal.md +97 -0
- package/standards/cicd/github-actions.md +567 -0
- package/standards/clean-code/naming.md +175 -0
- package/standards/clean-code/principles.md +179 -0
- package/standards/containerization/dockerfile.md +419 -0
- package/standards/database/selection-guide.md +443 -0
- package/standards/documentation/guidelines.md +189 -0
- package/standards/event-driven/domain-events.md +527 -0
- package/standards/kubernetes/deployment.md +518 -0
- package/standards/observability/guidelines.md +665 -0
- package/standards/project-setup/initialization-checklist.md +650 -0
- package/standards/spring-boot/best-practices.md +598 -0
- package/standards/testing/guidelines.md +559 -0
- package/standards/workflow/llm-development-workflow.md +542 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# Spring Boot Best Practices
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Best practices for Spring Boot 3.x development following modern Java conventions and enterprise patterns.
|
|
6
|
+
|
|
7
|
+
## Dependency Injection
|
|
8
|
+
|
|
9
|
+
### Use Constructor Injection
|
|
10
|
+
|
|
11
|
+
```java
|
|
12
|
+
// Bad - Field injection
|
|
13
|
+
@Service
|
|
14
|
+
public class OrderService {
|
|
15
|
+
@Autowired
|
|
16
|
+
private OrderRepository orderRepository;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Good - Constructor injection
|
|
20
|
+
@Service
|
|
21
|
+
public class OrderService {
|
|
22
|
+
private final OrderRepository orderRepository;
|
|
23
|
+
|
|
24
|
+
public OrderService(OrderRepository orderRepository) {
|
|
25
|
+
this.orderRepository = orderRepository;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Better - Lombok (if using)
|
|
30
|
+
@Service
|
|
31
|
+
@RequiredArgsConstructor
|
|
32
|
+
public class OrderService {
|
|
33
|
+
private final OrderRepository orderRepository;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Benefits:**
|
|
38
|
+
- Immutable dependencies (final)
|
|
39
|
+
- Explicit dependencies
|
|
40
|
+
- Easy to test
|
|
41
|
+
- Fails fast if dependency missing
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
### Use @ConfigurationProperties
|
|
46
|
+
|
|
47
|
+
```java
|
|
48
|
+
// Bad
|
|
49
|
+
@Value("${app.payment.timeout}")
|
|
50
|
+
private int timeout;
|
|
51
|
+
|
|
52
|
+
// Good
|
|
53
|
+
@ConfigurationProperties(prefix = "app.payment")
|
|
54
|
+
public record PaymentProperties(
|
|
55
|
+
int timeout,
|
|
56
|
+
String apiKey,
|
|
57
|
+
boolean retryEnabled,
|
|
58
|
+
RetryProperties retry
|
|
59
|
+
) {
|
|
60
|
+
public record RetryProperties(
|
|
61
|
+
int maxAttempts,
|
|
62
|
+
Duration backoff
|
|
63
|
+
) {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Configuration
|
|
67
|
+
@EnableConfigurationProperties(PaymentProperties.class)
|
|
68
|
+
public class PaymentConfig { }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Separate Configuration Classes
|
|
72
|
+
|
|
73
|
+
```java
|
|
74
|
+
@Configuration
|
|
75
|
+
public class WebConfig implements WebMvcConfigurer {
|
|
76
|
+
// Web-specific configuration
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Configuration
|
|
80
|
+
public class SecurityConfig {
|
|
81
|
+
// Security-specific configuration
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Configuration
|
|
85
|
+
public class PersistenceConfig {
|
|
86
|
+
// Database-specific configuration
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Configuration
|
|
90
|
+
public class KafkaConfig {
|
|
91
|
+
// Messaging-specific configuration
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## HTTP Clients
|
|
96
|
+
|
|
97
|
+
### When to Use HttpInterface vs RestClient
|
|
98
|
+
|
|
99
|
+
| Scenario | Recommended Client |
|
|
100
|
+
|----------|-------------------|
|
|
101
|
+
| Simple CRUD operations | HttpInterface |
|
|
102
|
+
| Standard REST endpoints | HttpInterface |
|
|
103
|
+
| Complex error handling | RestClient |
|
|
104
|
+
| Dynamic headers/query params | RestClient |
|
|
105
|
+
| Retry policies | RestClient |
|
|
106
|
+
| Circuit breaker integration | RestClient |
|
|
107
|
+
|
|
108
|
+
### HttpInterface (Declarative - Simple Cases)
|
|
109
|
+
|
|
110
|
+
Best for simple, standard REST API calls with minimal customization.
|
|
111
|
+
|
|
112
|
+
```java
|
|
113
|
+
// Define the client interface
|
|
114
|
+
@HttpExchange("/api/v1/users")
|
|
115
|
+
public interface UserClient {
|
|
116
|
+
|
|
117
|
+
@GetExchange("/{id}")
|
|
118
|
+
UserResponse getUser(@PathVariable String id);
|
|
119
|
+
|
|
120
|
+
@GetExchange
|
|
121
|
+
List<UserResponse> getAllUsers();
|
|
122
|
+
|
|
123
|
+
@PostExchange
|
|
124
|
+
UserResponse createUser(@RequestBody CreateUserRequest request);
|
|
125
|
+
|
|
126
|
+
@PutExchange("/{id}")
|
|
127
|
+
UserResponse updateUser(@PathVariable String id, @RequestBody UpdateUserRequest request);
|
|
128
|
+
|
|
129
|
+
@DeleteExchange("/{id}")
|
|
130
|
+
void deleteUser(@PathVariable String id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Configure the client
|
|
134
|
+
@Configuration
|
|
135
|
+
public class UserClientConfig {
|
|
136
|
+
|
|
137
|
+
@Bean
|
|
138
|
+
public UserClient userClient(RestClient.Builder builder) {
|
|
139
|
+
RestClient restClient = builder
|
|
140
|
+
.baseUrl("https://api.example.com")
|
|
141
|
+
.build();
|
|
142
|
+
|
|
143
|
+
HttpServiceProxyFactory factory = HttpServiceProxyFactory
|
|
144
|
+
.builderFor(RestClientAdapter.create(restClient))
|
|
145
|
+
.build();
|
|
146
|
+
|
|
147
|
+
return factory.createClient(UserClient.class);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### RestClient (Fluent API - Complex Cases)
|
|
153
|
+
|
|
154
|
+
Best for complex scenarios requiring fine-grained control.
|
|
155
|
+
|
|
156
|
+
```java
|
|
157
|
+
@Component
|
|
158
|
+
@RequiredArgsConstructor
|
|
159
|
+
public class PaymentGatewayClient {
|
|
160
|
+
|
|
161
|
+
private final RestClient restClient;
|
|
162
|
+
|
|
163
|
+
public PaymentResponse processPayment(PaymentRequest request) {
|
|
164
|
+
return restClient.post()
|
|
165
|
+
.uri("/payments")
|
|
166
|
+
.header("X-Idempotency-Key", request.idempotencyKey())
|
|
167
|
+
.header("X-Correlation-Id", MDC.get("correlationId"))
|
|
168
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
169
|
+
.body(request)
|
|
170
|
+
.retrieve()
|
|
171
|
+
.onStatus(HttpStatusCode::is4xxClientError, this::handleClientError)
|
|
172
|
+
.onStatus(HttpStatusCode::is5xxServerError, this::handleServerError)
|
|
173
|
+
.body(PaymentResponse.class);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public Optional<PaymentStatus> getPaymentStatus(String paymentId) {
|
|
177
|
+
return restClient.get()
|
|
178
|
+
.uri("/payments/{id}/status", paymentId)
|
|
179
|
+
.retrieve()
|
|
180
|
+
.onStatus(status -> status.value() == 404, (req, res) -> {})
|
|
181
|
+
.body(new ParameterizedTypeReference<Optional<PaymentStatus>>() {});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private void handleClientError(HttpRequest request, ClientHttpResponse response) {
|
|
185
|
+
// Custom error handling logic
|
|
186
|
+
throw new PaymentClientException("Payment failed: " + response.getStatusCode());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private void handleServerError(HttpRequest request, ClientHttpResponse response) {
|
|
190
|
+
throw new PaymentGatewayException("Gateway unavailable");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// RestClient configuration with interceptors
|
|
195
|
+
@Configuration
|
|
196
|
+
public class RestClientConfig {
|
|
197
|
+
|
|
198
|
+
@Bean
|
|
199
|
+
public RestClient paymentRestClient(RestClient.Builder builder) {
|
|
200
|
+
return builder
|
|
201
|
+
.baseUrl("https://payment.gateway.com/api/v1")
|
|
202
|
+
.defaultHeader("Authorization", "Bearer " + getToken())
|
|
203
|
+
.requestInterceptor(new LoggingInterceptor())
|
|
204
|
+
.requestInterceptor(new RetryInterceptor(3, Duration.ofMillis(500)))
|
|
205
|
+
.build();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## REST API Design
|
|
211
|
+
|
|
212
|
+
### Use Problem Details (RFC 7807)
|
|
213
|
+
|
|
214
|
+
```java
|
|
215
|
+
@RestControllerAdvice
|
|
216
|
+
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
|
217
|
+
|
|
218
|
+
@ExceptionHandler(OrderNotFoundException.class)
|
|
219
|
+
public ProblemDetail handleOrderNotFound(OrderNotFoundException ex) {
|
|
220
|
+
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
221
|
+
HttpStatus.NOT_FOUND,
|
|
222
|
+
ex.getMessage()
|
|
223
|
+
);
|
|
224
|
+
problem.setTitle("Order Not Found");
|
|
225
|
+
problem.setType(URI.create("https://api.example.com/errors/order-not-found"));
|
|
226
|
+
problem.setProperty("orderId", ex.getOrderId());
|
|
227
|
+
problem.setProperty("timestamp", Instant.now());
|
|
228
|
+
return problem;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@ExceptionHandler(BusinessRuleViolationException.class)
|
|
232
|
+
public ProblemDetail handleBusinessRuleViolation(BusinessRuleViolationException ex) {
|
|
233
|
+
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
234
|
+
HttpStatus.UNPROCESSABLE_ENTITY,
|
|
235
|
+
ex.getMessage()
|
|
236
|
+
);
|
|
237
|
+
problem.setTitle("Business Rule Violation");
|
|
238
|
+
problem.setType(URI.create("https://api.example.com/errors/business-rule-violation"));
|
|
239
|
+
problem.setProperty("rule", ex.getRuleName());
|
|
240
|
+
return problem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@ExceptionHandler(ConstraintViolationException.class)
|
|
244
|
+
public ProblemDetail handleValidation(ConstraintViolationException ex) {
|
|
245
|
+
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
246
|
+
HttpStatus.BAD_REQUEST,
|
|
247
|
+
"Validation failed"
|
|
248
|
+
);
|
|
249
|
+
problem.setTitle("Validation Error");
|
|
250
|
+
problem.setProperty("violations", ex.getConstraintViolations().stream()
|
|
251
|
+
.map(v -> Map.of(
|
|
252
|
+
"field", v.getPropertyPath().toString(),
|
|
253
|
+
"message", v.getMessage()
|
|
254
|
+
))
|
|
255
|
+
.toList());
|
|
256
|
+
return problem;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Use DTOs for API
|
|
262
|
+
|
|
263
|
+
```java
|
|
264
|
+
// Request DTO with validation
|
|
265
|
+
public record CreateOrderRequest(
|
|
266
|
+
@NotNull(message = "Customer ID is required")
|
|
267
|
+
String customerId,
|
|
268
|
+
|
|
269
|
+
@NotEmpty(message = "Order must have at least one line")
|
|
270
|
+
@Size(max = 100, message = "Order cannot have more than 100 lines")
|
|
271
|
+
List<@Valid OrderLineRequest> lines,
|
|
272
|
+
|
|
273
|
+
@Email(message = "Invalid email format")
|
|
274
|
+
String contactEmail
|
|
275
|
+
) {
|
|
276
|
+
public CreateOrderCommand toCommand() {
|
|
277
|
+
return new CreateOrderCommand(
|
|
278
|
+
CustomerId.of(customerId),
|
|
279
|
+
lines.stream().map(OrderLineRequest::toLine).toList()
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Response DTO
|
|
285
|
+
public record OrderResponse(
|
|
286
|
+
String id,
|
|
287
|
+
String status,
|
|
288
|
+
BigDecimal total,
|
|
289
|
+
List<OrderLineResponse> lines,
|
|
290
|
+
Instant createdAt
|
|
291
|
+
) {
|
|
292
|
+
public static OrderResponse from(Order order) {
|
|
293
|
+
return new OrderResponse(
|
|
294
|
+
order.getId().value(),
|
|
295
|
+
order.getStatus().name(),
|
|
296
|
+
order.getTotal().amount(),
|
|
297
|
+
order.getLines().stream().map(OrderLineResponse::from).toList(),
|
|
298
|
+
order.getCreatedAt()
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Controller Structure
|
|
305
|
+
|
|
306
|
+
```java
|
|
307
|
+
@RestController
|
|
308
|
+
@RequestMapping("/api/v1/orders")
|
|
309
|
+
@RequiredArgsConstructor
|
|
310
|
+
@Tag(name = "Orders", description = "Order management endpoints")
|
|
311
|
+
public class OrderController {
|
|
312
|
+
|
|
313
|
+
private final PlaceOrderUseCase placeOrderUseCase;
|
|
314
|
+
private final GetOrderUseCase getOrderUseCase;
|
|
315
|
+
private final ListOrdersUseCase listOrdersUseCase;
|
|
316
|
+
|
|
317
|
+
@PostMapping
|
|
318
|
+
@ResponseStatus(HttpStatus.CREATED)
|
|
319
|
+
@Operation(summary = "Create a new order")
|
|
320
|
+
@ApiResponse(responseCode = "201", description = "Order created successfully")
|
|
321
|
+
@ApiResponse(responseCode = "400", description = "Invalid request")
|
|
322
|
+
public OrderResponse createOrder(@Valid @RequestBody CreateOrderRequest request) {
|
|
323
|
+
Order order = placeOrderUseCase.execute(request.toCommand());
|
|
324
|
+
return OrderResponse.from(order);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@GetMapping("/{id}")
|
|
328
|
+
@Operation(summary = "Get order by ID")
|
|
329
|
+
@ApiResponse(responseCode = "200", description = "Order found")
|
|
330
|
+
@ApiResponse(responseCode = "404", description = "Order not found")
|
|
331
|
+
public OrderResponse getOrder(@PathVariable String id) {
|
|
332
|
+
return getOrderUseCase.execute(OrderId.of(id))
|
|
333
|
+
.map(OrderResponse::from)
|
|
334
|
+
.orElseThrow(() -> new OrderNotFoundException(id));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@GetMapping
|
|
338
|
+
@Operation(summary = "List orders with pagination")
|
|
339
|
+
public Page<OrderResponse> listOrders(
|
|
340
|
+
@RequestParam(defaultValue = "0") int page,
|
|
341
|
+
@RequestParam(defaultValue = "20") int size) {
|
|
342
|
+
return listOrdersUseCase.execute(PageRequest.of(page, size))
|
|
343
|
+
.map(OrderResponse::from);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Validation
|
|
349
|
+
|
|
350
|
+
### Use Bean Validation
|
|
351
|
+
|
|
352
|
+
```java
|
|
353
|
+
public record CreateOrderRequest(
|
|
354
|
+
@NotNull(message = "Customer ID is required")
|
|
355
|
+
String customerId,
|
|
356
|
+
|
|
357
|
+
@NotEmpty(message = "Order must have at least one line")
|
|
358
|
+
@Size(max = 100, message = "Order cannot have more than 100 lines")
|
|
359
|
+
List<@Valid OrderLineRequest> lines,
|
|
360
|
+
|
|
361
|
+
@Email(message = "Invalid email format")
|
|
362
|
+
String contactEmail,
|
|
363
|
+
|
|
364
|
+
@Pattern(regexp = "^[A-Z]{2}$", message = "Country code must be 2 uppercase letters")
|
|
365
|
+
String countryCode
|
|
366
|
+
) { }
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Custom Validators
|
|
370
|
+
|
|
371
|
+
```java
|
|
372
|
+
@Constraint(validatedBy = ValidOrderIdValidator.class)
|
|
373
|
+
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
|
374
|
+
@Retention(RetentionPolicy.RUNTIME)
|
|
375
|
+
public @interface ValidOrderId {
|
|
376
|
+
String message() default "Invalid order ID format";
|
|
377
|
+
Class<?>[] groups() default {};
|
|
378
|
+
Class<? extends Payload>[] payload() default {};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
public class ValidOrderIdValidator implements ConstraintValidator<ValidOrderId, String> {
|
|
382
|
+
private static final Pattern ORDER_ID_PATTERN = Pattern.compile("^ORD-[A-Z0-9]{8}$");
|
|
383
|
+
|
|
384
|
+
@Override
|
|
385
|
+
public boolean isValid(String value, ConstraintValidatorContext context) {
|
|
386
|
+
if (value == null) return true; // Use @NotNull for null checks
|
|
387
|
+
return ORDER_ID_PATTERN.matcher(value).matches();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Persistence
|
|
393
|
+
|
|
394
|
+
### Entity Design
|
|
395
|
+
|
|
396
|
+
```java
|
|
397
|
+
@Entity
|
|
398
|
+
@Table(name = "orders")
|
|
399
|
+
public class OrderJpaEntity {
|
|
400
|
+
|
|
401
|
+
@Id
|
|
402
|
+
private String id;
|
|
403
|
+
|
|
404
|
+
@Column(nullable = false)
|
|
405
|
+
private String customerId;
|
|
406
|
+
|
|
407
|
+
@Enumerated(EnumType.STRING)
|
|
408
|
+
@Column(nullable = false)
|
|
409
|
+
private OrderStatus status;
|
|
410
|
+
|
|
411
|
+
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
|
412
|
+
@JoinColumn(name = "order_id")
|
|
413
|
+
private List<OrderLineJpaEntity> lines = new ArrayList<>();
|
|
414
|
+
|
|
415
|
+
@Column(nullable = false, updatable = false)
|
|
416
|
+
private Instant createdAt;
|
|
417
|
+
|
|
418
|
+
@Column(nullable = false)
|
|
419
|
+
private Instant updatedAt;
|
|
420
|
+
|
|
421
|
+
// Domain entity conversion
|
|
422
|
+
public Order toDomain() {
|
|
423
|
+
return Order.reconstitute(
|
|
424
|
+
OrderId.of(id),
|
|
425
|
+
CustomerId.of(customerId),
|
|
426
|
+
status,
|
|
427
|
+
lines.stream().map(OrderLineJpaEntity::toDomain).toList(),
|
|
428
|
+
createdAt
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public static OrderJpaEntity fromDomain(Order order) {
|
|
433
|
+
OrderJpaEntity entity = new OrderJpaEntity();
|
|
434
|
+
entity.id = order.getId().value();
|
|
435
|
+
entity.customerId = order.getCustomerId().value();
|
|
436
|
+
entity.status = order.getStatus();
|
|
437
|
+
entity.lines = order.getLines().stream()
|
|
438
|
+
.map(OrderLineJpaEntity::fromDomain)
|
|
439
|
+
.toList();
|
|
440
|
+
entity.createdAt = order.getCreatedAt();
|
|
441
|
+
entity.updatedAt = Instant.now();
|
|
442
|
+
return entity;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Repository Pattern
|
|
448
|
+
|
|
449
|
+
```java
|
|
450
|
+
// Domain port (in domain layer)
|
|
451
|
+
public interface OrderRepository {
|
|
452
|
+
Optional<Order> findById(OrderId id);
|
|
453
|
+
List<Order> findByCustomerId(CustomerId customerId);
|
|
454
|
+
void save(Order order);
|
|
455
|
+
void delete(OrderId id);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// JPA Repository (in infrastructure layer)
|
|
459
|
+
public interface OrderJpaRepository extends JpaRepository<OrderJpaEntity, String> {
|
|
460
|
+
List<OrderJpaEntity> findByCustomerId(String customerId);
|
|
461
|
+
@Query("SELECT o FROM OrderJpaEntity o WHERE o.status = :status")
|
|
462
|
+
List<OrderJpaEntity> findByStatus(@Param("status") OrderStatus status);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Infrastructure adapter (implements domain port)
|
|
466
|
+
@Repository
|
|
467
|
+
@RequiredArgsConstructor
|
|
468
|
+
public class JpaOrderRepository implements OrderRepository {
|
|
469
|
+
|
|
470
|
+
private final OrderJpaRepository jpaRepository;
|
|
471
|
+
|
|
472
|
+
@Override
|
|
473
|
+
public Optional<Order> findById(OrderId id) {
|
|
474
|
+
return jpaRepository.findById(id.value())
|
|
475
|
+
.map(OrderJpaEntity::toDomain);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
@Override
|
|
479
|
+
public List<Order> findByCustomerId(CustomerId customerId) {
|
|
480
|
+
return jpaRepository.findByCustomerId(customerId.value())
|
|
481
|
+
.stream()
|
|
482
|
+
.map(OrderJpaEntity::toDomain)
|
|
483
|
+
.toList();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@Override
|
|
487
|
+
@Transactional
|
|
488
|
+
public void save(Order order) {
|
|
489
|
+
jpaRepository.save(OrderJpaEntity.fromDomain(order));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@Override
|
|
493
|
+
@Transactional
|
|
494
|
+
public void delete(OrderId id) {
|
|
495
|
+
jpaRepository.deleteById(id.value());
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Object Mapping with MapStruct
|
|
501
|
+
|
|
502
|
+
```java
|
|
503
|
+
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
|
|
504
|
+
public interface OrderMapper {
|
|
505
|
+
|
|
506
|
+
@Mapping(target = "id", source = "id.value")
|
|
507
|
+
@Mapping(target = "customerId", source = "customerId.value")
|
|
508
|
+
@Mapping(target = "total", source = "total.amount")
|
|
509
|
+
OrderResponse toResponse(Order order);
|
|
510
|
+
|
|
511
|
+
@Mapping(target = "id", expression = "java(OrderId.of(entity.getId()))")
|
|
512
|
+
@Mapping(target = "customerId", expression = "java(CustomerId.of(entity.getCustomerId()))")
|
|
513
|
+
Order toDomain(OrderJpaEntity entity);
|
|
514
|
+
|
|
515
|
+
@InheritInverseConfiguration
|
|
516
|
+
OrderJpaEntity toEntity(Order order);
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Slice Tests
|
|
521
|
+
|
|
522
|
+
### Web Layer Only
|
|
523
|
+
|
|
524
|
+
```java
|
|
525
|
+
@WebMvcTest(OrderController.class)
|
|
526
|
+
class OrderControllerTest {
|
|
527
|
+
@Autowired MockMvc mockMvc;
|
|
528
|
+
@MockBean PlaceOrderUseCase placeOrderUseCase;
|
|
529
|
+
@MockBean GetOrderUseCase getOrderUseCase;
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Repository Layer Only
|
|
534
|
+
|
|
535
|
+
```java
|
|
536
|
+
@DataJpaTest
|
|
537
|
+
class OrderJpaRepositoryTest {
|
|
538
|
+
@Autowired TestEntityManager entityManager;
|
|
539
|
+
@Autowired OrderJpaRepository repository;
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Security Practices
|
|
544
|
+
|
|
545
|
+
1. Never store secrets in code or properties files
|
|
546
|
+
2. Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault)
|
|
547
|
+
3. Enable HTTPS in production
|
|
548
|
+
4. Validate and sanitize all input
|
|
549
|
+
5. Use parameterized queries (JPA handles this)
|
|
550
|
+
6. Implement proper CORS configuration
|
|
551
|
+
7. Use Spring Security for authentication/authorization
|
|
552
|
+
8. Set security headers (CSP, X-Frame-Options, X-Content-Type-Options)
|
|
553
|
+
9. Implement rate limiting for public APIs
|
|
554
|
+
10. Use JWT with proper expiration and refresh tokens
|
|
555
|
+
|
|
556
|
+
## Application Properties Best Practices
|
|
557
|
+
|
|
558
|
+
```yaml
|
|
559
|
+
# application.yml
|
|
560
|
+
spring:
|
|
561
|
+
application:
|
|
562
|
+
name: order-service
|
|
563
|
+
|
|
564
|
+
datasource:
|
|
565
|
+
url: ${DATABASE_URL}
|
|
566
|
+
username: ${DATABASE_USERNAME}
|
|
567
|
+
password: ${DATABASE_PASSWORD}
|
|
568
|
+
|
|
569
|
+
jpa:
|
|
570
|
+
hibernate:
|
|
571
|
+
ddl-auto: validate # Never use create/update in production
|
|
572
|
+
open-in-view: false # Disable OSIV
|
|
573
|
+
|
|
574
|
+
jackson:
|
|
575
|
+
default-property-inclusion: non_null
|
|
576
|
+
serialization:
|
|
577
|
+
write-dates-as-timestamps: false
|
|
578
|
+
|
|
579
|
+
management:
|
|
580
|
+
endpoints:
|
|
581
|
+
web:
|
|
582
|
+
exposure:
|
|
583
|
+
include: health,info,metrics,prometheus
|
|
584
|
+
endpoint:
|
|
585
|
+
health:
|
|
586
|
+
show-details: when_authorized
|
|
587
|
+
probes:
|
|
588
|
+
enabled: true
|
|
589
|
+
|
|
590
|
+
server:
|
|
591
|
+
shutdown: graceful
|
|
592
|
+
tomcat:
|
|
593
|
+
connection-timeout: 5s
|
|
594
|
+
max-connections: 10000
|
|
595
|
+
threads:
|
|
596
|
+
max: 200
|
|
597
|
+
min-spare: 10
|
|
598
|
+
```
|