| name | implementation |
| description | Use when implementing any feature, adding code, or modifying existing code in this Java/Spring project. Triggers on write operations like adding entities, services, facades, controllers, or any domain logic. |
Implementation Guide
Overview
This project follows responsibility assignment, object collaboration, and expression consistency.
Quick Decision
digraph layer {
"Single domain?" [shape=diamond];
"Service" [shape=box];
"Multiple domains?" [shape=diamond];
"Facade" [shape=box];
"HTTP?" [shape=diamond];
"Controller" [shape=box];
"Single domain?" -> "Service" [label="yes"];
"Single domain?" -> "Multiple domains?" [label="no"];
"Multiple domains?" -> "Facade" [label="yes"];
"Multiple domains?" -> "HTTP?" [label="no"];
"HTTP?" -> "Controller" [label="yes"];
}
Critical Rules (Non-negotiable)
1. Controller Flow
ALWAYS: Controller -> Facade -> Service (never Controller -> Service)
@RestController
public class ProductV1Controller implements ProductV1ApiSpec {
private final ProductFacade productFacade;
public ProductV1Controller(ProductFacade productFacade) {
this.productFacade = productFacade;
}
}
See references/layer-boundaries.md for detailed patterns.
2. Layer Responsibilities
| Layer | @Transactional | Horizontal Dependencies | Why |
|---|
| Facade | When atomicity needed | Multiple Services OK | Wraps multiple Services in single transaction |
| Service | When atomicity needed | No other Services | Ensures atomicity within single domain |
readOnly usage: Master/Slave DB routing. Use readOnly=true for read-only queries to route to Slave DB.
Facade = COORDINATION ONLY - No business logic (if/when/switch). Delegate to Service/Entity.
See references/layer-boundaries.md for transaction boundaries and anti-patterns.
3. Error Handling
Required: CoreException + ErrorType (single exception type)
throw new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] ์ํฐํฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
See references/error-handling.md for ErrorType enum and patterns.
4. DTO Flow
Request.toCriteria() -> Criteria.toCommand() -> Command -> Entity -> Info.from() -> Response.from()
See references/dto-patterns.md for complete layer structure.
5. Domain Events
| Requirement | Pattern |
|---|
| Naming | {Action}EventV{n} (version suffix required) |
| Interface | Must implement DomainEvent |
| Fields | occurredAt: Instant required |
| Factory | public static from(entity) |
| Children | Use snapshots, not entity references |
public record OrderCreatedEventV1(
Long orderId,
List<OrderItemSnapshot> items, // Snapshot, not entity
Instant occurredAt
) implements DomainEvent {
public static OrderCreatedEventV1 from(Order order) {
return new OrderCreatedEventV1(order.getId(), order.getItemSnapshots(), Instant.now());
}
}
6. EventListener Pattern
| Type | Phase | Error Handling |
|---|
| Sync | BEFORE_COMMIT | Failure rolls back tx |
| Async | AFTER_COMMIT | try-catch required, log errors |
Always: @TransactionalEventListener(phase = TransactionPhase.XXX) - never plain @EventListener
Logging format: logger.info("[Event] {Action} start/complete - eventType: " + event.getClass().getSimpleName() + ", id: " + id)
7. Entity Encapsulation
Seven Rules:
- BaseEntity: ALL entities extend
BaseEntity (provides id, createdAt, updatedAt, deletedAt)
- @Table indexes: ALWAYS define indexes for query optimization
- private fields + @Getter: ALL mutable fields private, no
@Setter
- Behavior methods: State changes via domain verbs (
use(), pay()), not setters
- Immutable VOs:
final fields, operations return new instances
- Constructor/Factory validation: Validate in constructor or factory, never create invalid objects
- registerEvent(): Publish events for business-significant state changes
8. Naming Conventions
| Component | Pattern | Example |
|---|
| Controller | {Domain}V{n}Controller | ProductV1Controller |
| ApiSpec | {Domain}V{n}ApiSpec | ProductV1ApiSpec |
| Facade | {Domain}Facade | ProductFacade |
| Service | {Domain}Service | ProductService |
| Event | {Action}EventV{n} | OrderCreatedEventV1 |
| Query | {Domain}PageQuery | ProductPageQuery |
Methods: Domain verbs (use, expire, cancel), not technical (process, handle, execute)
Variables: Full names (totalAmount, quantity), not abbreviations (amt, qty)
Booleans: is{Adjective}, has{Noun}, canBe{Verb}
9. Domain Purity
Direction: interfaces -> application -> domain <- infrastructure
Domain imports NOTHING from other layers.
| Allowed in Domain | Forbidden in Domain |
|---|
JPA: @Entity, @Table, @Column | @Transactional |
@Component on Service | @JsonProperty, @JsonIgnore |
| Spring Data imports |
Repository Abstraction: Interface in domain, implementation in infrastructure.
10. Null Safety
| Rule | Pattern |
|---|
| Required fields | Non-nullable (never assign null) |
| Not found | if (x == null) throw new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] ...") |
| Optional | if (x != null) { ... }, Stream.of(a, b, c).filter(Objects::nonNull).toList() |
| Forbidden | Unchecked null dereference |
11. API Patterns
- ApiSpec interface: Swagger annotations go here, Controller implements it
- Query/PageQuery: Encapsulate pagination with compact constructor validation
public record ProductPageQuery(int page, int size) {
public ProductPageQuery {
if (page < 0) throw new CoreException(ErrorType.BAD_REQUEST, "[page = " + page + "] ํ์ด์ง๋ 0 ์ด์์ด์ด์ผ ํฉ๋๋ค.");
if (size < 1 || size > 100) throw new CoreException(ErrorType.BAD_REQUEST, "[size = " + size + "] ํ์ด์ง ํฌ๊ธฐ๋ 1~100์ด์ด์ผ ํฉ๋๋ค.");
}
}
record vs class selection:
- Default:
record + compact constructor โ for 2โ3 required parameters
- Allowed:
class + @Getter + private constructor + static factory โ when 4+ parameters AND optional parameters exist
- Validation must always occur in the constructor or static factory, regardless of choice
12. Messages
- Error messages: Korean,
[field = value] prefix AT START
- Javadoc: Korean
"[userId = " + userId + "] ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."
"์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. [userId = " + userId + "]"
13. Caching
| Rule | Pattern |
|---|
| Layer | Application Layer(Facade) ONLY |
| Pattern | Manual Cache-Aside with CacheTemplate |
| Cache Key | Sealed interface + TTL embedded |
| Cache Model | CachedXxxV1 versioned DTO (never Entity/Response) |
| List Caching | IDs only + separate Detail cache |
| Invalidation | Domain Event + @TransactionalEventListener(AFTER_COMMIT) |
See references/caching-patterns.md for detailed patterns, examples, and forbidden patterns.
Red Flags (Top 25)
| Thought | Reality |
|---|
| "Controller calling Service directly" | Controller -> Facade -> Service is MANDATORY |
| "Facade is unnecessary for simple cases" | Facade is ALWAYS required |
| "Service calling Service" | Coordinate in Facade |
| "Facade->Facade dependency" | Use domain events |
| "@Transactional on Service" | Only readOnly or managed in Facade |
"require() is fine" | Use CoreException: if (!cond) throw new CoreException(...) |
| "Domain exception per domain" | Single CoreException + ErrorType |
| "Return Entity directly" | DTO layer required |
| "English error message" | Korean with [field = value] prefix |
| "Entity without BaseEntity" | ALL entities MUST extend BaseEntity |
| "public field or @Setter" | ALL mutable fields need private + @Getter, no @Setter |
| "@Table without indexes" | ALWAYS define indexes |
| "Event without V1 suffix" | Version suffix required |
| "Just @EventListener" | Use @TransactionalEventListener with phase |
| "Async listener without try-catch" | Async failures must be caught and logged |
| "Inject JpaRepository directly" | Define interface in domain |
| "@JsonProperty in domain" | JSON is infrastructure concern |
| "Nullable for required fields" | Non-nullable by default |
| "Unchecked null dereference" | Use explicit null check + CoreException |
| "process/handle method names" | Use domain verbs |
| "Short variable names (amt, qty)" | Full descriptive names required |
| "Business logic in Facade" | Facade coordinates only, logic in Service/Entity |
| "External call inside @Transactional" | Use AFTER_COMMIT event listener |
| "Entity is just data holder" | Anemic domain model anti-pattern - entities MUST have behavior |
| "Skip validation in constructor/factory" | Invalid objects are forbidden |
| "@Cacheable is simpler" | Use CacheTemplate for control |
| "Cache in Service/Repository" | Caching belongs in Facade ONLY |
| "Cache Response directly" | Use CachedXxxV1 dedicated DTO |
| "String cache key" | Use sealed interface with TTL |
| "@CacheEvict allEntries" | Domain Event + selective evict |
References
Load these files ONLY when working on specific areas:
| File | When to Load |
|---|
references/layer-boundaries.md | Code placement, Service vs Facade, transactions |
references/error-handling.md | Exception creation, validation failures, ErrorType enum |
references/dto-patterns.md | API endpoints, Request/Response/Criteria/Command/Info classes |
references/domain-events.md | Event publishing, EventListener patterns, cross-domain communication |
references/entity-patterns.md | Entity design, encapsulation rules, null safety, domain purity |
references/naming-conventions.md | Method/variable/message naming, Korean messages |
references/api-patterns.md | ApiSpec interface, Query/PageQuery patterns |
references/caching-patterns.md | Cache-Aside in Facade, CacheKey sealed interface, CachedXxxV1 DTOs, invalidation |