Oh-my-toong-playground implementation
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.
git clone https://github.com/toongri/oh-my-toong-playground
T=$(mktemp -d) && git clone --depth=1 https://github.com/toongri/oh-my-toong-playground "$T" && mkdir -p ~/.claude/skills && cp -r "$T/projects/toong-java-spring-template/skills/implementation" ~/.claude/skills/toongri-oh-my-toong-playground-implementation-e57562 && rm -rf "$T"
projects/toong-java-spring-template/skills/implementation/SKILL.mdImplementation 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; // Facade, NOT Service 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 | (version suffix required) |
| Interface | Must implement |
| Fields | required |
| Factory | |
| 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
(provides id, createdAt, updatedAt, deletedAt)BaseEntity - @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()
), not setterspay() - Immutable VOs:
fields, operations return new instancesfinal - 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 | | |
| ApiSpec | | |
| Facade | | |
| Service | | |
| Event | | |
| Query | | |
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: , , | |
on Service | , |
| Spring Data imports |
Repository Abstraction: Interface in domain, implementation in infrastructure.
10. Null Safety
| Rule | Pattern |
|---|---|
| Required fields | Non-nullable (never assign ) |
| Not found | |
| Optional | , |
| 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:
+ compact constructor — for 2–3 required parametersrecord - Allowed:
+class
+ private constructor + static factory — when 4+ parameters AND optional parameters exist@Getter - Validation must always occur in the constructor or static factory, regardless of choice
12. Messages
- Error messages: Korean,
prefix AT START[field = value] - Javadoc: Korean
// Correct "[userId = " + userId + "] 사용자를 찾을 수 없습니다." // Wrong (prefix at end) "사용자를 찾을 수 없습니다. [userId = " + userId + "]"
13. Caching
| Rule | Pattern |
|---|---|
| Layer | Application Layer(Facade) ONLY |
| Pattern | Manual Cache-Aside with |
| Cache Key | Sealed interface + TTL embedded |
| Cache Model | versioned DTO (never Entity/Response) |
| List Caching | IDs only + separate Detail cache |
| Invalidation | Domain Event + |
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 |
" is fine" | Use : |
| "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 |
|---|---|
| Code placement, Service vs Facade, transactions |
| Exception creation, validation failures, ErrorType enum |
| API endpoints, Request/Response/Criteria/Command/Info classes |
| Event publishing, EventListener patterns, cross-domain communication |
| Entity design, encapsulation rules, null safety, domain purity |
| Method/variable/message naming, Korean messages |
| ApiSpec interface, Query/PageQuery patterns |
| Cache-Aside in Facade, CacheKey sealed interface, CachedXxxV1 DTOs, invalidation |