projects/toong-java-spring-template/skills/implementation/SKILL.md
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.
npx skillsauth add toongri/oh-my-toong-playground implementationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This project follows responsibility assignment, object collaboration, and expression consistency.
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"];
}
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.
| 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.
Required: CoreException + ErrorType (single exception type)
throw new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] 엔티티를 찾을 수 없습니다.");
See references/error-handling.md for ErrorType enum and patterns.
Request.toCriteria() -> Criteria.toCommand() -> Command -> Entity -> Info.from() -> Response.from()
See references/dto-patterns.md for complete layer structure.
| 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());
}
}
| 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)
Seven Rules:
BaseEntity (provides id, createdAt, updatedAt, deletedAt)@Setteruse(), pay()), not settersfinal fields, operations return new instances| 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}
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.
| 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 |
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:
record + compact constructor — for 2–3 required parametersclass + @Getter + private constructor + static factory — when 4+ parameters AND optional parameters exist[field = value] prefix AT START// Correct
"[userId = " + userId + "] 사용자를 찾을 수 없습니다."
// Wrong (prefix at end)
"사용자를 찾을 수 없습니다. [userId = " + userId + "]"
| 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.
| 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 |
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 |
tools
Use at the end of a work session to review the WHOLE session and record entities worth pinning. This is the manual, deliberate complete-sweep review — NOT an automated nudge. Triggers on "wrap up", "wrap-up", "session wrap", "end of session", "what should I pin".
documentation
Use when initializing the pins knowledge graph for the first time in a project. Guides the user through creating pins.yaml (the storage manifest). Triggers on "setup pins", "initialize pins", "create pins.yaml", "first-run pins".
testing
Use when you need to record a single pin entity to the knowledge graph. Invokes lib/pins record() to validate and write a canonical .md file. Triggers on "record pin", "pin this", "save this as a pin".
databases
Use when looking up pins by type, tags, or source. Drives lib/pins/query.ts to retrieve matching pin entries from the knowledge graph. Supersedes the legacy manual ls+frontmatter procedure.