mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 08:29:36 +01:00
feat: add Spring Boot ERP application with user management domain
Implement DDD-based architecture with domain, application, infrastructure, and API layers. Includes user/role management with authentication, RBAC permissions, audit logging, Liquibase migrations, and test suite.
This commit is contained in:
parent
a1df32377b
commit
ec9114aa0a
124 changed files with 18208 additions and 0 deletions
188
.claude/skills/ddd-implement/README.md
Normal file
188
.claude/skills/ddd-implement/README.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# DDD Implementation Skill
|
||||
|
||||
A Claude Code skill that acts as a Senior DDD Developer, implementing domain-driven code following Clean Architecture principles.
|
||||
|
||||
## What This Skill Does
|
||||
|
||||
This skill helps you implement DDD code that follows established patterns and rules:
|
||||
|
||||
✅ **Aggregates** - With proper invariant enforcement and Result types
|
||||
✅ **Entities** - Child entities within aggregates
|
||||
✅ **Value Objects** - Immutable, self-validating
|
||||
✅ **Use Cases** - In application layer with proper error handling
|
||||
✅ **Repositories** - Interfaces in domain, implementations in infrastructure
|
||||
✅ **Domain Events** - For cross-aggregate communication
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
**Use `/ddd-implement` when**:
|
||||
- You have a domain model designed (from `/ddd-model`)
|
||||
- You need to implement specific aggregates, entities, or value objects
|
||||
- You want code that follows DDD rules automatically
|
||||
- You need proper error handling (Result types for Java, errors for Go)
|
||||
- You want layer boundaries respected
|
||||
|
||||
**Don't use this skill for**:
|
||||
- Domain modeling and design (use `/ddd-model` instead)
|
||||
- Generic coding tasks (use default Claude Code)
|
||||
- Non-DDD projects
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Implement an Aggregate
|
||||
|
||||
```bash
|
||||
/ddd-implement --lang=java "Implement Order aggregate with addLineItem, removeLineItem, and cancel methods"
|
||||
```
|
||||
|
||||
The skill will:
|
||||
1. Create the aggregate in `domain/order/`
|
||||
2. Add proper invariants (e.g., "Cannot modify cancelled order")
|
||||
3. Use Result types for error handling
|
||||
4. Implement sealed interfaces for errors
|
||||
5. Add domain events (OrderCreated, OrderCancelled, etc.)
|
||||
|
||||
### Implement a Use Case
|
||||
|
||||
```bash
|
||||
/ddd-implement --lang=go "Implement PlaceOrder use case that creates an order and reserves inventory"
|
||||
```
|
||||
|
||||
The skill will:
|
||||
1. Create use case in `application/order/`
|
||||
2. Use repository interfaces (not implementations)
|
||||
3. Handle errors properly
|
||||
4. Return DTOs (not domain objects)
|
||||
5. Add transaction boundaries if needed
|
||||
|
||||
### Implement a Repository
|
||||
|
||||
```bash
|
||||
/ddd-implement --lang=java "Implement PostgreSQL repository for Order aggregate"
|
||||
```
|
||||
|
||||
The skill will:
|
||||
1. Create implementation in `infrastructure/order/persistence/`
|
||||
2. Implement the domain interface
|
||||
3. Add exception boundary (catch SQL exceptions → return domain errors)
|
||||
4. Map between domain model and database schema
|
||||
|
||||
### Implement from Existing File
|
||||
|
||||
```bash
|
||||
/ddd-implement internal/domain/account/account.go
|
||||
```
|
||||
|
||||
The skill will:
|
||||
1. Detect language from file extension
|
||||
2. Read existing code
|
||||
3. Suggest improvements or complete partial implementations
|
||||
4. Follow established patterns in the file
|
||||
|
||||
## What Makes This Skill Different
|
||||
|
||||
### Enforces DDD Rules Automatically
|
||||
|
||||
The skill knows and enforces:
|
||||
- Aggregate boundaries (no direct aggregate-to-aggregate references)
|
||||
- Invariant documentation and enforcement
|
||||
- Entity equality (ID-based only)
|
||||
- Value Object immutability
|
||||
- Repository patterns (interface in domain, impl in infrastructure)
|
||||
- Layer dependencies (domain has no external deps)
|
||||
|
||||
### Language-Aware
|
||||
|
||||
**For Java**:
|
||||
- Uses Result<E, T> types (no exceptions from domain/application)
|
||||
- Uses sealed interfaces for errors
|
||||
- Uses pattern matching with switch expressions
|
||||
- Uses static imports for Failure/Success
|
||||
- Follows Java 21+ conventions
|
||||
|
||||
**For Go**:
|
||||
- Uses error return values
|
||||
- Uses pointer receivers for aggregates/entities
|
||||
- Uses value receivers for value objects
|
||||
- Uses sentinel errors and custom error types
|
||||
- Follows Uber Go Style Guide
|
||||
|
||||
### Error Handling Built-In
|
||||
|
||||
The skill automatically:
|
||||
- Returns Result types (Java) or errors (Go)
|
||||
- Creates layer-specific error types
|
||||
- Adds exception boundaries at infrastructure layer
|
||||
- Logs errors appropriately (ERROR/WARN/INFO levels)
|
||||
- Prevents silent failures
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Analyzes your request** - Determines what to implement (aggregate, use case, etc.)
|
||||
2. **Detects language** - From flags, file extensions, or project structure
|
||||
3. **Loads rules** - DDD rules, error handling, style guides for your language
|
||||
4. **Generates code** - Following templates and patterns
|
||||
5. **Validates** - Checks against DDD rules before completion
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
/ddd-implement
|
||||
├── SKILL.md # Skill manifest (loaded by Claude Code)
|
||||
├── system-prompt.md # Core instructions for the implementation agent
|
||||
├── README.md # This file
|
||||
└── examples/
|
||||
├── go-example.md # Example session in Go
|
||||
└── java-example.md # Example session in Java
|
||||
```
|
||||
|
||||
The `system-prompt.md` references rules from the `ddd-model` skill:
|
||||
- `ddd-model/rules/ddd-rules.md` - Core DDD patterns
|
||||
- `ddd-model/rules/error-handling.md` - Error handling strategy
|
||||
- `ddd-model/languages/{lang}/style-guide.md` - Language conventions
|
||||
- `ddd-model/languages/{lang}/templates/` - Code templates
|
||||
|
||||
## Workflow: Modeling → Implementation
|
||||
|
||||
```
|
||||
1. Design with /ddd-model
|
||||
↓ (identifies aggregates, entities, invariants)
|
||||
|
||||
2. Implement with /ddd-implement
|
||||
↓ (generates code following rules)
|
||||
|
||||
3. Review with /review (or code review)
|
||||
↓ (validates DDD principles)
|
||||
|
||||
4. Iterate
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See detailed examples in:
|
||||
- [Java Example Session](./examples/java-example.md)
|
||||
- [Go Example Session](./examples/go-example.md)
|
||||
|
||||
## Rules Reference
|
||||
|
||||
This skill enforces rules from:
|
||||
|
||||
- **DDD Rules**: [ddd-model/rules/ddd-rules.md](../ddd-model/rules/ddd-rules.md)
|
||||
- **Clean Architecture**: [ddd-model/rules/clean-arch.md](../ddd-model/rules/clean-arch.md)
|
||||
- **Error Handling**: [ddd-model/rules/error-handling.md](../ddd-model/rules/error-handling.md)
|
||||
- **Java Style**: [ddd-model/languages/java/style-guide.md](../ddd-model/languages/java/style-guide.md)
|
||||
- **Go Style**: [ddd-model/languages/go/style-guide.md](../ddd-model/languages/go/style-guide.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
To improve this skill:
|
||||
|
||||
1. **Add examples** - Real-world implementation sessions in `examples/`
|
||||
2. **Refine rules** - Update `system-prompt.md` based on experience
|
||||
3. **Add templates** - Language-specific templates in `ddd-model/languages/{lang}/templates/`
|
||||
4. **Document patterns** - Special patterns in `ddd-model/rules/`
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **ddd-model** - For domain modeling and design
|
||||
- **review** - For code review with DDD principles (if available)
|
||||
66
.claude/skills/ddd-implement/SKILL.md
Normal file
66
.claude/skills/ddd-implement/SKILL.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# DDD Implementation Skill
|
||||
|
||||
**Skill Name**: `ddd-implement`
|
||||
**Aliases**: `implement`, `ddd-code`
|
||||
**Version**: 1.0.0
|
||||
|
||||
## Description
|
||||
|
||||
Senior DDD developer that implements domain-driven code following Clean Architecture principles. Understands tactical DDD patterns, layer boundaries, and language-specific conventions.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/ddd-implement [--lang=go|java] [file-or-description]
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Implement a specific aggregate
|
||||
/ddd-implement --lang=java "Implement Order aggregate with addLineItem and cancel methods"
|
||||
|
||||
# Implement in existing file
|
||||
/ddd-implement --lang=go internal/domain/account/account.go
|
||||
|
||||
# Implement use case
|
||||
/ddd-implement --lang=java "Implement TransferMoney use case in application layer"
|
||||
|
||||
# Implement repository
|
||||
/ddd-implement --lang=go "Implement PostgreSQL repository for Account aggregate"
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
This skill can:
|
||||
- ✅ Implement **Aggregates** with proper invariant enforcement
|
||||
- ✅ Implement **Value Objects** with validation
|
||||
- ✅ Implement **Entities** (child entities within aggregates)
|
||||
- ✅ Implement **Use Cases** in application layer
|
||||
- ✅ Implement **Repositories** (interface + implementation)
|
||||
- ✅ Implement **Domain Events**
|
||||
- ✅ Follow **Error Handling** patterns (Result types for Java, errors for Go)
|
||||
- ✅ Respect **Layer boundaries** (domain, application, infrastructure)
|
||||
- ✅ Enforce **DDD rules** (aggregate boundaries, invariants, etc.)
|
||||
|
||||
## What This Skill Does NOT Do
|
||||
|
||||
- ❌ **Modeling/Design** - Use `/ddd-model` skill for that
|
||||
- ❌ **Infrastructure concerns** - Only implements adapters, not databases/HTTP servers themselves
|
||||
- ❌ **Tests** - Focuses on implementation (you can ask for tests separately)
|
||||
|
||||
## System Prompt
|
||||
|
||||
<system-prompt src="./system-prompt.md" />
|
||||
|
||||
## Language Detection
|
||||
|
||||
The skill detects language in this order:
|
||||
1. `--lang=` flag
|
||||
2. File extension (`.go`, `.java`)
|
||||
3. Current working directory structure
|
||||
4. Ask user if unclear
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **ddd-model** - For domain modeling and design
|
||||
- **review** - For code review with DDD principles
|
||||
595
.claude/skills/ddd-implement/examples/go-example.md
Normal file
595
.claude/skills/ddd-implement/examples/go-example.md
Normal file
|
|
@ -0,0 +1,595 @@
|
|||
# Go Implementation Example Session
|
||||
|
||||
This is an example session showing how the `/ddd-implement` skill works in a Go project.
|
||||
|
||||
## Scenario
|
||||
|
||||
You're building a reservation system and need to implement the **Booking** aggregate in the domain layer.
|
||||
|
||||
---
|
||||
|
||||
## User Request
|
||||
|
||||
```
|
||||
/ddd-implement --lang=go "Implement Booking aggregate with these requirements:
|
||||
- Booking has ID, room ID, guest ID, check-in/check-out dates, and status
|
||||
- Can confirm booking (transitions to CONFIRMED)
|
||||
- Can cancel booking (transitions to CANCELLED)
|
||||
- Cannot modify after checked in
|
||||
- Check-out date must be after check-in date"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent Response
|
||||
|
||||
I'll implement the **Booking aggregate** in the domain layer following DDD patterns for Go.
|
||||
|
||||
### Analysis
|
||||
|
||||
**Aggregate Root**: Booking
|
||||
**Value Objects**:
|
||||
- BookingID (unique identifier)
|
||||
- RoomID (reference to Room aggregate)
|
||||
- GuestID (reference to Guest aggregate)
|
||||
- DateRange (check-in to check-out)
|
||||
- BookingStatus (Pending|Confirmed|Cancelled|CheckedIn|CheckedOut)
|
||||
|
||||
**Invariants**:
|
||||
1. Check-out date must be after check-in date
|
||||
2. Cannot modify booking after checked in
|
||||
3. Can only confirm booking in Pending status
|
||||
4. Can only cancel before checked in
|
||||
|
||||
**Layer**: Domain (`internal/domain/booking`)
|
||||
|
||||
Let me implement this:
|
||||
|
||||
### Error Types
|
||||
|
||||
```go
|
||||
package booking
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidDateRange = errors.New("check-out date must be after check-in date")
|
||||
ErrBookingAlreadyConfirmed = errors.New("booking is already confirmed")
|
||||
ErrBookingCancelled = errors.New("booking is cancelled")
|
||||
ErrBookingCheckedIn = errors.New("cannot modify booking after check-in")
|
||||
ErrInvalidStatus = errors.New("invalid status transition")
|
||||
)
|
||||
|
||||
// BookingNotFoundError represents a specific booking not found error
|
||||
type BookingNotFoundError struct {
|
||||
BookingID BookingID
|
||||
}
|
||||
|
||||
func (e BookingNotFoundError) Error() string {
|
||||
return "booking not found: " + e.BookingID.Value()
|
||||
}
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
```go
|
||||
package booking
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BookingID uniquely identifies a booking
|
||||
type BookingID struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func NewBookingID(value string) (BookingID, error) {
|
||||
if value == "" {
|
||||
return BookingID{}, errors.New("booking ID cannot be empty")
|
||||
}
|
||||
return BookingID{value: value}, nil
|
||||
}
|
||||
|
||||
func GenerateBookingID() BookingID {
|
||||
return BookingID{value: uuid.New().String()}
|
||||
}
|
||||
|
||||
func (id BookingID) Value() string {
|
||||
return id.value
|
||||
}
|
||||
|
||||
func (id BookingID) Equals(other BookingID) bool {
|
||||
return id.value == other.value
|
||||
}
|
||||
|
||||
// RoomID references a Room aggregate
|
||||
type RoomID struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func NewRoomID(value string) (RoomID, error) {
|
||||
if value == "" {
|
||||
return RoomID{}, errors.New("room ID cannot be empty")
|
||||
}
|
||||
return RoomID{value: value}, nil
|
||||
}
|
||||
|
||||
func (id RoomID) Value() string {
|
||||
return id.value
|
||||
}
|
||||
|
||||
// GuestID references a Guest aggregate
|
||||
type GuestID struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func NewGuestID(value string) (GuestID, error) {
|
||||
if value == "" {
|
||||
return GuestID{}, errors.New("guest ID cannot be empty")
|
||||
}
|
||||
return GuestID{value: value}, nil
|
||||
}
|
||||
|
||||
func (id GuestID) Value() string {
|
||||
return id.value
|
||||
}
|
||||
|
||||
// DateRange represents check-in to check-out period
|
||||
type DateRange struct {
|
||||
checkIn time.Time
|
||||
checkOut time.Time
|
||||
}
|
||||
|
||||
// NewDateRange creates a DateRange with validation
|
||||
func NewDateRange(checkIn, checkOut time.Time) (DateRange, error) {
|
||||
// Invariant: Check-out must be after check-in
|
||||
if !checkOut.After(checkIn) {
|
||||
return DateRange{}, ErrInvalidDateRange
|
||||
}
|
||||
|
||||
return DateRange{
|
||||
checkIn: checkIn,
|
||||
checkOut: checkOut,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dr DateRange) CheckIn() time.Time {
|
||||
return dr.checkIn
|
||||
}
|
||||
|
||||
func (dr DateRange) CheckOut() time.Time {
|
||||
return dr.checkOut
|
||||
}
|
||||
|
||||
func (dr DateRange) Nights() int {
|
||||
duration := dr.checkOut.Sub(dr.checkIn)
|
||||
return int(duration.Hours() / 24)
|
||||
}
|
||||
|
||||
func (dr DateRange) Contains(date time.Time) bool {
|
||||
return (date.Equal(dr.checkIn) || date.After(dr.checkIn)) &&
|
||||
date.Before(dr.checkOut)
|
||||
}
|
||||
|
||||
// BookingStatus represents the current status of a booking
|
||||
type BookingStatus int
|
||||
|
||||
const (
|
||||
BookingStatusPending BookingStatus = iota
|
||||
BookingStatusConfirmed
|
||||
BookingStatusCancelled
|
||||
BookingStatusCheckedIn
|
||||
BookingStatusCheckedOut
|
||||
)
|
||||
|
||||
func (s BookingStatus) String() string {
|
||||
switch s {
|
||||
case BookingStatusPending:
|
||||
return "PENDING"
|
||||
case BookingStatusConfirmed:
|
||||
return "CONFIRMED"
|
||||
case BookingStatusCancelled:
|
||||
return "CANCELLED"
|
||||
case BookingStatusCheckedIn:
|
||||
return "CHECKED_IN"
|
||||
case BookingStatusCheckedOut:
|
||||
return "CHECKED_OUT"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Booking Aggregate
|
||||
|
||||
```go
|
||||
package booking
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Booking aggregate root.
|
||||
//
|
||||
// Invariants:
|
||||
// - Check-out date must be after check-in date
|
||||
// - Cannot modify booking after checked in
|
||||
// - Can only confirm booking in Pending status
|
||||
// - Can only cancel before checked in
|
||||
type Booking struct {
|
||||
id BookingID
|
||||
roomID RoomID
|
||||
guestID GuestID
|
||||
dateRange DateRange
|
||||
status BookingStatus
|
||||
|
||||
createdAt time.Time
|
||||
updatedAt time.Time
|
||||
}
|
||||
|
||||
// NewBooking creates a new booking in Pending status.
|
||||
func NewBooking(
|
||||
id BookingID,
|
||||
roomID RoomID,
|
||||
guestID GuestID,
|
||||
dateRange DateRange,
|
||||
) (*Booking, error) {
|
||||
now := time.Now()
|
||||
|
||||
return &Booking{
|
||||
id: id,
|
||||
roomID: roomID,
|
||||
guestID: guestID,
|
||||
dateRange: dateRange,
|
||||
status: BookingStatusPending,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Getters
|
||||
func (b *Booking) ID() BookingID { return b.id }
|
||||
func (b *Booking) RoomID() RoomID { return b.roomID }
|
||||
func (b *Booking) GuestID() GuestID { return b.guestID }
|
||||
func (b *Booking) DateRange() DateRange { return b.dateRange }
|
||||
func (b *Booking) Status() BookingStatus { return b.status }
|
||||
func (b *Booking) CreatedAt() time.Time { return b.createdAt }
|
||||
func (b *Booking) UpdatedAt() time.Time { return b.updatedAt }
|
||||
|
||||
// Confirm transitions booking from Pending to Confirmed.
|
||||
//
|
||||
// Invariant: Can only confirm Pending bookings
|
||||
func (b *Booking) Confirm() error {
|
||||
// Guard: Check status
|
||||
if b.status == BookingStatusConfirmed {
|
||||
return ErrBookingAlreadyConfirmed
|
||||
}
|
||||
if b.status == BookingStatusCancelled {
|
||||
return ErrBookingCancelled
|
||||
}
|
||||
if b.status == BookingStatusCheckedIn || b.status == BookingStatusCheckedOut {
|
||||
return ErrBookingCheckedIn
|
||||
}
|
||||
|
||||
// Transition to Confirmed
|
||||
b.status = BookingStatusConfirmed
|
||||
b.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancel transitions booking to Cancelled.
|
||||
//
|
||||
// Invariant: Cannot cancel after checked in
|
||||
func (b *Booking) Cancel() error {
|
||||
// Guard: Already cancelled
|
||||
if b.status == BookingStatusCancelled {
|
||||
return nil // Idempotent
|
||||
}
|
||||
|
||||
// Guard: Cannot cancel after check-in
|
||||
if b.status == BookingStatusCheckedIn || b.status == BookingStatusCheckedOut {
|
||||
return ErrBookingCheckedIn
|
||||
}
|
||||
|
||||
// Transition to Cancelled
|
||||
b.status = BookingStatusCancelled
|
||||
b.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckIn transitions booking to CheckedIn status.
|
||||
//
|
||||
// Invariant: Can only check in Confirmed bookings
|
||||
func (b *Booking) CheckIn() error {
|
||||
// Guard: Must be confirmed
|
||||
if b.status != BookingStatusConfirmed {
|
||||
if b.status == BookingStatusCancelled {
|
||||
return ErrBookingCancelled
|
||||
}
|
||||
if b.status == BookingStatusCheckedIn {
|
||||
return nil // Idempotent
|
||||
}
|
||||
return ErrInvalidStatus
|
||||
}
|
||||
|
||||
// Transition to CheckedIn
|
||||
b.status = BookingStatusCheckedIn
|
||||
b.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckOut transitions booking to CheckedOut status.
|
||||
//
|
||||
// Invariant: Can only check out after checked in
|
||||
func (b *Booking) CheckOut() error {
|
||||
// Guard: Must be checked in
|
||||
if b.status != BookingStatusCheckedIn {
|
||||
if b.status == BookingStatusCancelled {
|
||||
return ErrBookingCancelled
|
||||
}
|
||||
if b.status == BookingStatusCheckedOut {
|
||||
return nil // Idempotent
|
||||
}
|
||||
return ErrInvalidStatus
|
||||
}
|
||||
|
||||
// Transition to CheckedOut
|
||||
b.status = BookingStatusCheckedOut
|
||||
b.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDates changes the check-in/check-out dates.
|
||||
//
|
||||
// Invariant: Cannot modify after checked in
|
||||
// Invariant: New dates must be valid (check-out after check-in)
|
||||
func (b *Booking) UpdateDates(newDateRange DateRange) error {
|
||||
// Guard: Cannot modify after check-in
|
||||
if b.status == BookingStatusCheckedIn || b.status == BookingStatusCheckedOut {
|
||||
return ErrBookingCheckedIn
|
||||
}
|
||||
|
||||
// Guard: Cannot modify cancelled booking
|
||||
if b.status == BookingStatusCancelled {
|
||||
return ErrBookingCancelled
|
||||
}
|
||||
|
||||
// Update dates
|
||||
b.dateRange = newDateRange
|
||||
b.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Interface
|
||||
|
||||
```go
|
||||
package booking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Repository defines persistence operations for Booking aggregates.
|
||||
type Repository interface {
|
||||
// Save persists a booking (insert or update)
|
||||
Save(ctx context.Context, booking *Booking) error
|
||||
|
||||
// FindByID retrieves a booking by ID
|
||||
FindByID(ctx context.Context, id BookingID) (*Booking, error)
|
||||
|
||||
// FindByGuest retrieves all bookings for a guest
|
||||
FindByGuest(ctx context.Context, guestID GuestID) ([]*Booking, error)
|
||||
|
||||
// FindByRoom retrieves all bookings for a room
|
||||
FindByRoom(ctx context.Context, roomID RoomID) ([]*Booking, error)
|
||||
|
||||
// FindOverlapping finds bookings that overlap with the given date range for a room
|
||||
FindOverlapping(ctx context.Context, roomID RoomID, dateRange DateRange) ([]*Booking, error)
|
||||
|
||||
// Delete removes a booking
|
||||
Delete(ctx context.Context, id BookingID) error
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Service (Optional)
|
||||
|
||||
```go
|
||||
package booking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service provides domain operations for bookings.
|
||||
type Service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
// NewService creates a booking service.
|
||||
func NewService(repo Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
// CheckAvailability checks if a room is available for the given dates.
|
||||
//
|
||||
// This is a domain service because it queries across multiple bookings.
|
||||
func (s *Service) CheckAvailability(
|
||||
ctx context.Context,
|
||||
roomID RoomID,
|
||||
dateRange DateRange,
|
||||
) (bool, error) {
|
||||
// Find overlapping bookings
|
||||
overlapping, err := s.repo.FindOverlapping(ctx, roomID, dateRange)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if any active bookings overlap
|
||||
for _, booking := range overlapping {
|
||||
// Ignore cancelled bookings
|
||||
if booking.status == BookingStatusCancelled {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found an active booking that overlaps
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Room is available
|
||||
return true, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Implemented**:
|
||||
- Booking aggregate root with all required operations
|
||||
- Value objects: BookingID, RoomID, GuestID, DateRange, BookingStatus
|
||||
- Custom error types for domain-specific errors
|
||||
- Repository interface in domain layer
|
||||
- Optional domain service for availability checking
|
||||
|
||||
✅ **DDD Rules Followed**:
|
||||
- Aggregate is only entry point
|
||||
- All invariants documented in comments
|
||||
- Invariants checked in NewBooking and mutation methods
|
||||
- No direct references to other aggregates (RoomID, GuestID are IDs only)
|
||||
- Pointer receiver for aggregate (*Booking)
|
||||
- Value receiver for value objects (DateRange)
|
||||
- One aggregate = one transaction boundary
|
||||
|
||||
✅ **Go Conventions**:
|
||||
- Unexported fields, exported methods
|
||||
- Constructor function (NewBooking)
|
||||
- Error return values (not panics)
|
||||
- Sentinel errors (ErrInvalidDateRange, etc.)
|
||||
- Custom error types (BookingNotFoundError)
|
||||
|
||||
✅ **Layer**: Domain (no external dependencies)
|
||||
|
||||
✅ **Files Created**:
|
||||
- `internal/domain/booking/booking.go` - Aggregate root
|
||||
- `internal/domain/booking/value_objects.go` - Value objects and IDs
|
||||
- `internal/domain/booking/errors.go` - Domain errors
|
||||
- `internal/domain/booking/repository.go` - Repository interface
|
||||
- `internal/domain/booking/service.go` - Domain service
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
You can now:
|
||||
|
||||
1. **Implement the repository**:
|
||||
```
|
||||
/ddd-implement --lang=go "Implement PostgreSQL repository for Booking aggregate"
|
||||
```
|
||||
|
||||
2. **Implement a use case**:
|
||||
```
|
||||
/ddd-implement --lang=go "Create CreateBooking use case in application layer"
|
||||
```
|
||||
|
||||
3. **Add tests**:
|
||||
```
|
||||
Write unit tests for Booking aggregate
|
||||
```
|
||||
|
||||
4. **Implement HTTP handler**:
|
||||
```
|
||||
/ddd-implement --lang=go "Create HTTP handler for booking endpoints in infrastructure layer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Example
|
||||
|
||||
Here's how you would test the Booking aggregate:
|
||||
|
||||
```go
|
||||
package booking_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"myapp/internal/domain/booking"
|
||||
)
|
||||
|
||||
func TestBooking_Confirm_FromPending_Succeeds(t *testing.T) {
|
||||
// Arrange
|
||||
id := booking.GenerateBookingID()
|
||||
roomID, _ := booking.NewRoomID("room-123")
|
||||
guestID, _ := booking.NewGuestID("guest-456")
|
||||
checkIn := time.Now().AddDate(0, 0, 1)
|
||||
checkOut := time.Now().AddDate(0, 0, 3)
|
||||
dateRange, _ := booking.NewDateRange(checkIn, checkOut)
|
||||
|
||||
b, err := booking.NewBooking(id, roomID, guestID, dateRange)
|
||||
if err != nil {
|
||||
t.Fatalf("NewBooking failed: %v", err)
|
||||
}
|
||||
|
||||
// Act
|
||||
err = b.Confirm()
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Errorf("Confirm failed: %v", err)
|
||||
}
|
||||
|
||||
if b.Status() != booking.BookingStatusConfirmed {
|
||||
t.Errorf("Expected status CONFIRMED, got %v", b.Status())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBooking_Cancel_AfterCheckIn_Fails(t *testing.T) {
|
||||
// Arrange
|
||||
id := booking.GenerateBookingID()
|
||||
roomID, _ := booking.NewRoomID("room-123")
|
||||
guestID, _ := booking.NewGuestID("guest-456")
|
||||
checkIn := time.Now().AddDate(0, 0, 1)
|
||||
checkOut := time.Now().AddDate(0, 0, 3)
|
||||
dateRange, _ := booking.NewDateRange(checkIn, checkOut)
|
||||
|
||||
b, _ := booking.NewBooking(id, roomID, guestID, dateRange)
|
||||
_ = b.Confirm()
|
||||
_ = b.CheckIn()
|
||||
|
||||
// Act
|
||||
err := b.Cancel()
|
||||
|
||||
// Assert
|
||||
if err != booking.ErrBookingCheckedIn {
|
||||
t.Errorf("Expected ErrBookingCheckedIn, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateRange_InvalidDates_Fails(t *testing.T) {
|
||||
// Arrange
|
||||
checkIn := time.Now().AddDate(0, 0, 3)
|
||||
checkOut := time.Now().AddDate(0, 0, 1) // Before check-in!
|
||||
|
||||
// Act
|
||||
_, err := booking.NewDateRange(checkIn, checkOut)
|
||||
|
||||
// Assert
|
||||
if err != booking.ErrInvalidDateRange {
|
||||
t.Errorf("Expected ErrInvalidDateRange, got %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
637
.claude/skills/ddd-implement/examples/java-example.md
Normal file
637
.claude/skills/ddd-implement/examples/java-example.md
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
# Java Implementation Example Session
|
||||
|
||||
This is an example session showing how the `/ddd-implement` skill works in a Java project.
|
||||
|
||||
## Scenario
|
||||
|
||||
You're building an e-commerce system and need to implement the **Order** aggregate in the domain layer.
|
||||
|
||||
---
|
||||
|
||||
## User Request
|
||||
|
||||
```
|
||||
/ddd-implement --lang=java "Implement Order aggregate with these requirements:
|
||||
- Order has order number, customer ID, list of line items, and status
|
||||
- Can add/remove line items (only when status is DRAFT)
|
||||
- Can place order (transitions to PLACED)
|
||||
- Can cancel order (transitions to CANCELLED)
|
||||
- Cannot modify after PLACED or CANCELLED
|
||||
- Total amount is calculated from line items"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent Response
|
||||
|
||||
I'll implement the **Order aggregate** in the domain layer following DDD patterns.
|
||||
|
||||
### Analysis
|
||||
|
||||
**Aggregate Root**: Order
|
||||
**Entities**: OrderLine (line items within order)
|
||||
**Value Objects**:
|
||||
- OrderNumber (unique identifier)
|
||||
- CustomerId (reference to Customer aggregate)
|
||||
- Money (for prices/totals)
|
||||
- OrderStatus (enum: DRAFT, PLACED, CANCELLED)
|
||||
|
||||
**Invariants**:
|
||||
1. Cannot modify order after PLACED or CANCELLED
|
||||
2. Cannot place order with zero line items
|
||||
3. Line items must have positive quantity
|
||||
4. Order total = sum of line item totals
|
||||
|
||||
**Layer**: Domain (`com.example.domain.order`)
|
||||
|
||||
Let me implement this:
|
||||
|
||||
### Error Types
|
||||
|
||||
```java
|
||||
package com.example.domain.order;
|
||||
|
||||
/**
|
||||
* Order domain errors.
|
||||
*/
|
||||
public sealed interface OrderError permits
|
||||
OrderAlreadyPlacedError,
|
||||
OrderCancelledError,
|
||||
EmptyOrderError,
|
||||
LineItemNotFoundError,
|
||||
InvalidLineItemError {
|
||||
String message();
|
||||
}
|
||||
|
||||
public record OrderAlreadyPlacedError(
|
||||
OrderNumber orderNumber
|
||||
) implements OrderError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Order already placed: " + orderNumber;
|
||||
}
|
||||
}
|
||||
|
||||
public record OrderCancelledError(
|
||||
OrderNumber orderNumber
|
||||
) implements OrderError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Order is cancelled: " + orderNumber;
|
||||
}
|
||||
}
|
||||
|
||||
public record EmptyOrderError() implements OrderError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Cannot place order with no line items";
|
||||
}
|
||||
}
|
||||
|
||||
public record LineItemNotFoundError(
|
||||
OrderLineId lineItemId
|
||||
) implements OrderError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Line item not found: " + lineItemId;
|
||||
}
|
||||
}
|
||||
|
||||
public record InvalidLineItemError(
|
||||
String reason
|
||||
) implements OrderError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Invalid line item: " + reason;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Order Aggregate
|
||||
|
||||
```java
|
||||
package com.example.domain.order;
|
||||
|
||||
import com.example.shared.result.Result;
|
||||
import static com.example.shared.result.Result.Failure;
|
||||
import static com.example.shared.result.Result.Success;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Order aggregate root.
|
||||
*
|
||||
* Invariant: Cannot modify order after PLACED or CANCELLED
|
||||
* Invariant: Cannot place order with zero line items
|
||||
* Invariant: Order total = sum of line item totals
|
||||
*/
|
||||
public class Order {
|
||||
private final OrderNumber orderNumber;
|
||||
private final CustomerId customerId;
|
||||
private final List<OrderLine> lineItems;
|
||||
private OrderStatus status;
|
||||
private final Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
|
||||
/**
|
||||
* Private constructor - use factory method.
|
||||
*/
|
||||
private Order(
|
||||
OrderNumber orderNumber,
|
||||
CustomerId customerId,
|
||||
OrderStatus status,
|
||||
Instant createdAt
|
||||
) {
|
||||
this.orderNumber = orderNumber;
|
||||
this.customerId = customerId;
|
||||
this.lineItems = new ArrayList<>();
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Order in DRAFT status.
|
||||
*/
|
||||
public static Result<OrderError, Order> create(
|
||||
OrderNumber orderNumber,
|
||||
CustomerId customerId
|
||||
) {
|
||||
Order order = new Order(
|
||||
orderNumber,
|
||||
customerId,
|
||||
OrderStatus.DRAFT,
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
return Result.success(order);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public OrderNumber orderNumber() { return orderNumber; }
|
||||
public CustomerId customerId() { return customerId; }
|
||||
public OrderStatus status() { return status; }
|
||||
public Instant createdAt() { return createdAt; }
|
||||
public Instant updatedAt() { return updatedAt; }
|
||||
|
||||
/**
|
||||
* Returns defensive copy of line items.
|
||||
*/
|
||||
public List<OrderLine> lineItems() {
|
||||
return List.copyOf(lineItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total order amount.
|
||||
*
|
||||
* Invariant: Total = sum of all line item totals
|
||||
*/
|
||||
public Money calculateTotal() {
|
||||
Money total = Money.usd(0);
|
||||
|
||||
for (OrderLine line : lineItems) {
|
||||
Money lineTotal = line.calculateTotal();
|
||||
total = switch (total.add(lineTotal)) {
|
||||
case Failure(MoneyError error) ->
|
||||
throw new IllegalStateException(
|
||||
"Currency mismatch in order: " + error.message()
|
||||
);
|
||||
case Success(Money sum) -> sum;
|
||||
};
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a line item to the order.
|
||||
*
|
||||
* Invariant: Cannot modify after PLACED or CANCELLED
|
||||
*/
|
||||
public Result<OrderError, Void> addLineItem(
|
||||
ProductId productId,
|
||||
String productName,
|
||||
Money unitPrice,
|
||||
int quantity
|
||||
) {
|
||||
// Guard: Check order status
|
||||
if (status == OrderStatus.PLACED) {
|
||||
return Result.failure(new OrderAlreadyPlacedError(orderNumber));
|
||||
}
|
||||
if (status == OrderStatus.CANCELLED) {
|
||||
return Result.failure(new OrderCancelledError(orderNumber));
|
||||
}
|
||||
|
||||
// Create line item
|
||||
OrderLineId lineId = OrderLineId.generate();
|
||||
|
||||
return switch (OrderLine.create(lineId, productId, productName, unitPrice, quantity)) {
|
||||
case Failure(OrderLineError error) ->
|
||||
Result.failure(new InvalidLineItemError(error.message()));
|
||||
case Success(OrderLine line) -> {
|
||||
lineItems.add(line);
|
||||
this.updatedAt = Instant.now();
|
||||
yield Result.success(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a line item from the order.
|
||||
*
|
||||
* Invariant: Cannot modify after PLACED or CANCELLED
|
||||
*/
|
||||
public Result<OrderError, Void> removeLineItem(OrderLineId lineItemId) {
|
||||
// Guard: Check order status
|
||||
if (status == OrderStatus.PLACED) {
|
||||
return Result.failure(new OrderAlreadyPlacedError(orderNumber));
|
||||
}
|
||||
if (status == OrderStatus.CANCELLED) {
|
||||
return Result.failure(new OrderCancelledError(orderNumber));
|
||||
}
|
||||
|
||||
// Find and remove line item
|
||||
boolean removed = lineItems.removeIf(line -> line.id().equals(lineItemId));
|
||||
|
||||
if (!removed) {
|
||||
return Result.failure(new LineItemNotFoundError(lineItemId));
|
||||
}
|
||||
|
||||
this.updatedAt = Instant.now();
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates quantity of an existing line item.
|
||||
*
|
||||
* Invariant: Cannot modify after PLACED or CANCELLED
|
||||
*/
|
||||
public Result<OrderError, Void> updateLineItemQuantity(
|
||||
OrderLineId lineItemId,
|
||||
int newQuantity
|
||||
) {
|
||||
// Guard: Check order status
|
||||
if (status == OrderStatus.PLACED) {
|
||||
return Result.failure(new OrderAlreadyPlacedError(orderNumber));
|
||||
}
|
||||
if (status == OrderStatus.CANCELLED) {
|
||||
return Result.failure(new OrderCancelledError(orderNumber));
|
||||
}
|
||||
|
||||
// Find line item
|
||||
OrderLine line = findLineItemById(lineItemId);
|
||||
if (line == null) {
|
||||
return Result.failure(new LineItemNotFoundError(lineItemId));
|
||||
}
|
||||
|
||||
// Update quantity
|
||||
return switch (line.updateQuantity(newQuantity)) {
|
||||
case Failure(OrderLineError error) ->
|
||||
Result.failure(new InvalidLineItemError(error.message()));
|
||||
case Success(Void ignored) -> {
|
||||
this.updatedAt = Instant.now();
|
||||
yield Result.success(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the order (transitions from DRAFT to PLACED).
|
||||
*
|
||||
* Invariant: Cannot place empty order
|
||||
* Invariant: Can only place order in DRAFT status
|
||||
*/
|
||||
public Result<OrderError, Void> place() {
|
||||
// Guard: Check status
|
||||
if (status == OrderStatus.PLACED) {
|
||||
return Result.failure(new OrderAlreadyPlacedError(orderNumber));
|
||||
}
|
||||
if (status == OrderStatus.CANCELLED) {
|
||||
return Result.failure(new OrderCancelledError(orderNumber));
|
||||
}
|
||||
|
||||
// Guard: Check has line items
|
||||
if (lineItems.isEmpty()) {
|
||||
return Result.failure(new EmptyOrderError());
|
||||
}
|
||||
|
||||
// Transition to PLACED
|
||||
this.status = OrderStatus.PLACED;
|
||||
this.updatedAt = Instant.now();
|
||||
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the order.
|
||||
*
|
||||
* Invariant: Can cancel from DRAFT or PLACED, not from CANCELLED
|
||||
*/
|
||||
public Result<OrderError, Void> cancel() {
|
||||
// Guard: Already cancelled
|
||||
if (status == OrderStatus.CANCELLED) {
|
||||
return Result.failure(new OrderCancelledError(orderNumber));
|
||||
}
|
||||
|
||||
// Transition to CANCELLED
|
||||
this.status = OrderStatus.CANCELLED;
|
||||
this.updatedAt = Instant.now();
|
||||
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
private OrderLine findLineItemById(OrderLineId id) {
|
||||
return lineItems.stream()
|
||||
.filter(line -> line.id().equals(id))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Order order)) return false;
|
||||
return Objects.equals(orderNumber, order.orderNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(orderNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Order{" +
|
||||
"orderNumber=" + orderNumber +
|
||||
", customerId=" + customerId +
|
||||
", status=" + status +
|
||||
", lineItems=" + lineItems.size() +
|
||||
", total=" + calculateTotal() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OrderLine Entity
|
||||
|
||||
```java
|
||||
package com.example.domain.order;
|
||||
|
||||
import com.example.shared.result.Result;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* OrderLine errors.
|
||||
*/
|
||||
public sealed interface OrderLineError permits
|
||||
InvalidQuantityError,
|
||||
InvalidPriceError {
|
||||
String message();
|
||||
}
|
||||
|
||||
public record InvalidQuantityError(int quantity) implements OrderLineError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Quantity must be positive: " + quantity;
|
||||
}
|
||||
}
|
||||
|
||||
public record InvalidPriceError(Money price) implements OrderLineError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Price must be positive: " + price;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderLine entity (child entity within Order aggregate).
|
||||
*
|
||||
* Invariant: Quantity must be positive
|
||||
* Invariant: Price must be positive
|
||||
*/
|
||||
public class OrderLine {
|
||||
private final OrderLineId id;
|
||||
private final ProductId productId;
|
||||
private final String productName;
|
||||
private final Money unitPrice;
|
||||
private int quantity;
|
||||
|
||||
/**
|
||||
* Package-private constructor - created by Order aggregate.
|
||||
*/
|
||||
OrderLine(
|
||||
OrderLineId id,
|
||||
ProductId productId,
|
||||
String productName,
|
||||
Money unitPrice,
|
||||
int quantity
|
||||
) {
|
||||
this.id = id;
|
||||
this.productId = productId;
|
||||
this.productName = productName;
|
||||
this.unitPrice = unitPrice;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new OrderLine.
|
||||
*/
|
||||
static Result<OrderLineError, OrderLine> create(
|
||||
OrderLineId id,
|
||||
ProductId productId,
|
||||
String productName,
|
||||
Money unitPrice,
|
||||
int quantity
|
||||
) {
|
||||
// Invariant: Quantity must be positive
|
||||
if (quantity <= 0) {
|
||||
return Result.failure(new InvalidQuantityError(quantity));
|
||||
}
|
||||
|
||||
// Invariant: Price must be positive
|
||||
if (unitPrice.isNegativeOrZero()) {
|
||||
return Result.failure(new InvalidPriceError(unitPrice));
|
||||
}
|
||||
|
||||
return Result.success(new OrderLine(
|
||||
id,
|
||||
productId,
|
||||
productName,
|
||||
unitPrice,
|
||||
quantity
|
||||
));
|
||||
}
|
||||
|
||||
public OrderLineId id() { return id; }
|
||||
public ProductId productId() { return productId; }
|
||||
public String productName() { return productName; }
|
||||
public Money unitPrice() { return unitPrice; }
|
||||
public int quantity() { return quantity; }
|
||||
|
||||
/**
|
||||
* Updates the quantity.
|
||||
*
|
||||
* Invariant: Quantity must be positive
|
||||
*/
|
||||
Result<OrderLineError, Void> updateQuantity(int newQuantity) {
|
||||
if (newQuantity <= 0) {
|
||||
return Result.failure(new InvalidQuantityError(newQuantity));
|
||||
}
|
||||
|
||||
this.quantity = newQuantity;
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total price for this line.
|
||||
*/
|
||||
public Money calculateTotal() {
|
||||
return unitPrice.multiply(quantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equality based on ID only (entity identity).
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof OrderLine that)) return false;
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OrderLine{" +
|
||||
"id=" + id +
|
||||
", product=" + productName +
|
||||
", quantity=" + quantity +
|
||||
", unitPrice=" + unitPrice +
|
||||
", total=" + calculateTotal() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supporting Types
|
||||
|
||||
```java
|
||||
package com.example.domain.order;
|
||||
|
||||
/**
|
||||
* Order status enumeration.
|
||||
*/
|
||||
public enum OrderStatus {
|
||||
DRAFT("Order is being created"),
|
||||
PLACED("Order has been placed"),
|
||||
CANCELLED("Order was cancelled");
|
||||
|
||||
private final String description;
|
||||
|
||||
OrderStatus(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderNumber value object.
|
||||
*/
|
||||
public record OrderNumber(String value) {
|
||||
public OrderNumber {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("OrderNumber cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public static OrderNumber of(String value) {
|
||||
return new OrderNumber(value);
|
||||
}
|
||||
|
||||
public static OrderNumber generate() {
|
||||
return new OrderNumber("ORD-" + java.util.UUID.randomUUID());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderLineId value object.
|
||||
*/
|
||||
public record OrderLineId(String value) {
|
||||
public OrderLineId {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("OrderLineId cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public static OrderLineId generate() {
|
||||
return new OrderLineId(java.util.UUID.randomUUID().toString());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **Implemented**:
|
||||
- Order aggregate root with all required operations
|
||||
- OrderLine entity (child entity, package-private constructor)
|
||||
- Sealed interfaces for error types
|
||||
- All invariants documented and enforced
|
||||
- Result types for all operations (no exceptions)
|
||||
- Pattern matching with switch expressions
|
||||
- Defensive copy for line items collection
|
||||
|
||||
✅ **DDD Rules Followed**:
|
||||
- Aggregate is only entry point (line items accessed via Order methods)
|
||||
- Entities have ID-based equality
|
||||
- All invariants checked in constructor and mutation methods
|
||||
- No direct references to other aggregates (ProductId, CustomerId are IDs only)
|
||||
- One aggregate = one transaction boundary
|
||||
|
||||
✅ **Layer**: Domain (no external dependencies)
|
||||
|
||||
✅ **Files Created**:
|
||||
- `com/example/domain/order/Order.java`
|
||||
- `com/example/domain/order/OrderLine.java`
|
||||
- `com/example/domain/order/OrderError.java`
|
||||
- `com/example/domain/order/OrderLineError.java`
|
||||
- `com/example/domain/order/OrderStatus.java`
|
||||
- `com/example/domain/order/OrderNumber.java`
|
||||
- `com/example/domain/order/OrderLineId.java`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
You can now:
|
||||
|
||||
1. **Implement the repository interface**:
|
||||
```
|
||||
/ddd-implement --lang=java "Create OrderRepository interface in domain layer"
|
||||
```
|
||||
|
||||
2. **Implement a use case**:
|
||||
```
|
||||
/ddd-implement --lang=java "Create PlaceOrder use case in application layer"
|
||||
```
|
||||
|
||||
3. **Implement repository**:
|
||||
```
|
||||
/ddd-implement --lang=java "Implement JdbcOrderRepository in infrastructure layer"
|
||||
```
|
||||
|
||||
4. **Add tests**:
|
||||
```
|
||||
Write unit tests for Order aggregate
|
||||
```
|
||||
787
.claude/skills/ddd-implement/system-prompt.md
Normal file
787
.claude/skills/ddd-implement/system-prompt.md
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
# DDD Implementation Agent - System Prompt
|
||||
|
||||
You are a **Senior Software Engineer** specializing in **Domain-Driven Design (DDD)** and **Clean Architecture**. Your expertise includes:
|
||||
|
||||
- Tactical DDD patterns (Aggregates, Entities, Value Objects, Domain Events)
|
||||
- Clean Architecture with strict layer separation
|
||||
- Language-specific best practices (Go, Java 21+)
|
||||
- Error handling patterns (Result types, domain errors)
|
||||
- Invariant enforcement and business rule validation
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Implement domain-driven code** following established patterns
|
||||
2. **Enforce DDD rules** at all times
|
||||
3. **Respect layer boundaries** (domain → application → infrastructure)
|
||||
4. **Write clean, maintainable code** following language conventions
|
||||
5. **Document invariants** clearly in code
|
||||
6. **Use appropriate error handling** for the target language
|
||||
|
||||
---
|
||||
|
||||
## Language-Specific Rules
|
||||
|
||||
### For Java Projects
|
||||
|
||||
**Load these rules**:
|
||||
- [Java Error Handling](../ddd-model/languages/java/error-handling.md)
|
||||
- [Java Style Guide](../ddd-model/languages/java/style-guide.md)
|
||||
- [Java Project Structure](../ddd-model/languages/java/structure.md)
|
||||
|
||||
**Key Conventions**:
|
||||
- ✅ Use **Result<E, T>** types (Error left, Value right)
|
||||
- ✅ Use **sealed interfaces** for error types
|
||||
- ✅ Use **pattern matching** with switch expressions
|
||||
- ✅ Use **static imports** for `Failure` and `Success`
|
||||
- ✅ Use **records** for simple Value Objects (exception-based) or **classes** for Result-based
|
||||
- ✅ Use **private constructors** + **public static factory methods**
|
||||
- ✅ Mark methods **package-private** for entities (created by aggregate)
|
||||
- ✅ Use **Java 21+** features (records, sealed interfaces, pattern matching)
|
||||
- ❌ **NO exceptions** from domain/application layer
|
||||
- ❌ **NO getOrElse()** - forces explicit error handling
|
||||
- ❌ **NO silent failures** - all errors must be handled or propagated
|
||||
|
||||
**Example Code Style**:
|
||||
```java
|
||||
public class Account {
|
||||
private Money balance;
|
||||
|
||||
// Private constructor
|
||||
private Account(Money balance) {
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
// Factory method returning Result
|
||||
public static Result<AccountError, Account> create(Money initialBalance) {
|
||||
if (initialBalance.isNegative()) {
|
||||
return Result.failure(new NegativeBalanceError(initialBalance));
|
||||
}
|
||||
return Result.success(new Account(initialBalance));
|
||||
}
|
||||
|
||||
// Mutation returning Result
|
||||
public Result<AccountError, Void> withdraw(Money amount) {
|
||||
return switch (balance.subtract(amount)) {
|
||||
case Failure(MoneyError error) ->
|
||||
Result.failure(new InvalidAmountError(error.message()));
|
||||
case Success(Money newBalance) -> {
|
||||
if (newBalance.isNegative()) {
|
||||
yield Result.failure(new InsufficientFundsError(balance, amount));
|
||||
}
|
||||
this.balance = newBalance;
|
||||
yield Result.success(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Go Projects
|
||||
|
||||
**Load these rules**:
|
||||
- [Go Style Guide](../ddd-model/languages/go/style-guide.md)
|
||||
- [Go Project Structure](../ddd-model/languages/go/structure.md)
|
||||
|
||||
**Key Conventions**:
|
||||
- ✅ Use **pointer receivers** for Aggregates and Entities
|
||||
- ✅ Use **value receivers** for Value Objects
|
||||
- ✅ Return **error** as last return value
|
||||
- ✅ Use **sentinel errors** (`var ErrNotFound = errors.New(...)`)
|
||||
- ✅ Use **custom error types** for rich errors
|
||||
- ✅ Use **constructor functions** (`NewAccount`, `NewMoney`)
|
||||
- ✅ Use **MustXxx** variants for tests only
|
||||
- ✅ **Unexported fields**, exported methods
|
||||
- ✅ Use **compile-time interface checks** (`var _ Repository = (*PostgresRepo)(nil)`)
|
||||
- ❌ **NO panics** in domain/application code (only in tests with Must functions)
|
||||
|
||||
**Example Code Style**:
|
||||
```go
|
||||
// Account aggregate with pointer receiver
|
||||
type Account struct {
|
||||
id AccountID
|
||||
balance Money
|
||||
status Status
|
||||
}
|
||||
|
||||
// Constructor returning pointer and error
|
||||
func NewAccount(id AccountID, initialBalance Money) (*Account, error) {
|
||||
if initialBalance.IsNegative() {
|
||||
return nil, ErrNegativeBalance
|
||||
}
|
||||
return &Account{
|
||||
id: id,
|
||||
balance: initialBalance,
|
||||
status: StatusActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Mutation method with pointer receiver
|
||||
func (a *Account) Withdraw(amount Money) error {
|
||||
if a.status == StatusClosed {
|
||||
return ErrAccountClosed
|
||||
}
|
||||
|
||||
newBalance, err := a.balance.Subtract(amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newBalance.IsNegative() {
|
||||
return ErrInsufficientFunds
|
||||
}
|
||||
|
||||
a.balance = newBalance
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DDD Rules (MANDATORY)
|
||||
|
||||
**Load complete rules from**:
|
||||
- [DDD Rules](../ddd-model/rules/ddd-rules.md)
|
||||
- [Clean Architecture](../ddd-model/rules/clean-arch.md)
|
||||
- [Invariants Guide](../ddd-model/rules/invariants.md)
|
||||
- [Degraded State Pattern](../ddd-model/rules/degraded-state-pattern.md)
|
||||
|
||||
**Critical Rules to Enforce**:
|
||||
|
||||
### 1. Aggregate Rules
|
||||
- ✅ Aggregate Root is the ONLY public entry point
|
||||
- ✅ Child entities accessed ONLY via aggregate methods
|
||||
- ✅ NO direct references to other aggregates (use IDs only)
|
||||
- ✅ One aggregate = one transaction boundary
|
||||
- ✅ All invariants documented with `// Invariant:` or `@Invariant` comments
|
||||
- ✅ Invariants checked in constructor AND mutation methods
|
||||
|
||||
**Example**:
|
||||
```java
|
||||
/**
|
||||
* Account aggregate root.
|
||||
*
|
||||
* Invariant: Balance >= 0 for standard accounts
|
||||
* Invariant: Must have at least one OWNER holder
|
||||
*/
|
||||
public class Account {
|
||||
// Invariant enforced in constructor
|
||||
public static Result<AccountError, Account> create(...) {
|
||||
// Check invariants
|
||||
}
|
||||
|
||||
// Invariant enforced in withdraw
|
||||
public Result<AccountError, Void> withdraw(Money amount) {
|
||||
// Check invariants
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Entity Rules
|
||||
- ✅ **Package-private constructor** (created by aggregate)
|
||||
- ✅ **Equality based on ID only**
|
||||
- ✅ **No public factory methods** (aggregate creates entities)
|
||||
- ✅ **Local invariants only** (aggregate handles aggregate-wide invariants)
|
||||
|
||||
**Example** (Java):
|
||||
```java
|
||||
public class Holder {
|
||||
private final HolderID id;
|
||||
private HolderRole role;
|
||||
|
||||
// Package-private - created by Account aggregate
|
||||
Holder(HolderID id, HolderRole role) {
|
||||
this.id = id;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
// Package-private mutation
|
||||
Result<HolderError, Void> changeRole(HolderRole newRole) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
// Equality based on ID only!
|
||||
return Objects.equals(id, ((Holder) o).id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Value Object Rules
|
||||
- ✅ **Immutable** (no setters, final fields)
|
||||
- ✅ **Validation in constructor** or factory method
|
||||
- ✅ **Equality compares ALL fields**
|
||||
- ✅ **Operations return NEW instances**
|
||||
- ✅ **Self-validating** (invalid state impossible)
|
||||
|
||||
**Example** (Java with Result):
|
||||
```java
|
||||
public class Money {
|
||||
private final long amountInCents;
|
||||
private final String currency;
|
||||
|
||||
private Money(long amountInCents, String currency) {
|
||||
this.amountInCents = amountInCents;
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
public static Result<MoneyError, Money> create(long amount, String currency) {
|
||||
if (currency == null || currency.length() != 3) {
|
||||
return Result.failure(new InvalidCurrencyError(currency));
|
||||
}
|
||||
return Result.success(new Money(amount, currency));
|
||||
}
|
||||
|
||||
// Operations return NEW instances
|
||||
public Result<MoneyError, Money> add(Money other) {
|
||||
if (!this.currency.equals(other.currency)) {
|
||||
return Result.failure(new CurrencyMismatchError(...));
|
||||
}
|
||||
return Result.success(new Money(
|
||||
this.amountInCents + other.amountInCents,
|
||||
this.currency
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
// Compare ALL fields
|
||||
return amountInCents == other.amountInCents
|
||||
&& currency.equals(other.currency);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Repository Rules
|
||||
- ✅ **Interface in domain layer**
|
||||
- ✅ **Implementation in infrastructure layer**
|
||||
- ✅ **Operate on aggregates only** (not entities)
|
||||
- ✅ **Return Result types** (Java) or **error** (Go)
|
||||
- ✅ **Domain-specific errors** (AccountNotFoundError, not generic exceptions)
|
||||
|
||||
**Example** (Java):
|
||||
```java
|
||||
// Domain layer: internal/domain/account/repository.java
|
||||
public interface AccountRepository {
|
||||
Result<RepositoryError, Void> save(Account account);
|
||||
Result<RepositoryError, Account> findById(AccountID id);
|
||||
}
|
||||
|
||||
// Infrastructure layer: internal/infrastructure/account/persistence/jdbc_repository.java
|
||||
public class JdbcAccountRepository implements AccountRepository {
|
||||
@Override
|
||||
public Result<RepositoryError, Void> save(Account account) {
|
||||
try {
|
||||
// JDBC implementation
|
||||
return Result.success(null);
|
||||
} catch (SQLException e) {
|
||||
log.error("Failed to save account", e); // Log at ERROR
|
||||
return Result.failure(new DatabaseError(e.getMessage())); // Return domain error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Layer Boundary Rules
|
||||
- ✅ **Domain** → NO external dependencies (pure business logic)
|
||||
- ✅ **Application** → depends on domain ONLY (orchestrates use cases)
|
||||
- ✅ **Infrastructure** → depends on domain (implements interfaces)
|
||||
- ❌ **NO** domain importing infrastructure
|
||||
- ❌ **NO** domain importing application
|
||||
|
||||
**Directory structure validation**:
|
||||
```
|
||||
✅ internal/domain/account/ imports nothing external
|
||||
✅ internal/application/account/ imports internal/domain/account
|
||||
✅ internal/infrastructure/account/ imports internal/domain/account
|
||||
❌ internal/domain/account/ imports internal/infrastructure/ # FORBIDDEN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
### Java: Result Types
|
||||
|
||||
**All domain and application methods return Result<E, T>**:
|
||||
|
||||
```java
|
||||
// Domain layer
|
||||
public Result<AccountError, Void> withdraw(Money amount) {
|
||||
// Returns domain errors
|
||||
}
|
||||
|
||||
// Application layer
|
||||
public Result<ApplicationError, AccountDTO> execute(WithdrawCommand cmd) {
|
||||
return switch (accountRepo.findById(cmd.accountId())) {
|
||||
case Failure(RepositoryError error) -> {
|
||||
log.error("Repository error: {}", error.message());
|
||||
yield Result.failure(new InfrastructureError(error.message()));
|
||||
}
|
||||
case Success(Account account) ->
|
||||
switch (account.withdraw(cmd.amount())) {
|
||||
case Failure(AccountError error) -> {
|
||||
log.warn("Domain error: {}", error.message());
|
||||
yield Result.failure(new InvalidOperationError(error.message()));
|
||||
}
|
||||
case Success(Void ignored) -> {
|
||||
accountRepo.save(account);
|
||||
yield Result.success(toDTO(account));
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Infrastructure layer - exception boundary
|
||||
public Result<RepositoryError, Account> findById(AccountID id) {
|
||||
try {
|
||||
// JDBC code that throws SQLException
|
||||
} catch (SQLException e) {
|
||||
log.error("Database error", e); // Log original exception at ERROR level
|
||||
return Result.failure(new DatabaseError(e.getMessage())); // Return domain error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Logging strategy**:
|
||||
- Domain errors → **WARN** level (business rule violations)
|
||||
- Application errors → **WARN/INFO** level
|
||||
- Infrastructure errors → **ERROR** level (technical failures)
|
||||
- When transforming errors → log original at **TRACE** level
|
||||
|
||||
### Go: Error Returns
|
||||
|
||||
```go
|
||||
// Domain layer
|
||||
func (a *Account) Withdraw(amount Money) error {
|
||||
if a.balance.LessThan(amount) {
|
||||
return ErrInsufficientFunds // Domain error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Application layer
|
||||
func (uc *WithdrawMoney) Execute(ctx context.Context, cmd WithdrawCommand) (*AccountDTO, error) {
|
||||
account, err := uc.accountRepo.FindByID(ctx, cmd.AccountID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrAccountNotFound) {
|
||||
return nil, ErrAccountNotFoundApp // Application error
|
||||
}
|
||||
return nil, fmt.Errorf("repository error: %w", err)
|
||||
}
|
||||
|
||||
if err := account.Withdraw(cmd.Amount); err != nil {
|
||||
return nil, fmt.Errorf("withdraw failed: %w", err) // Wrap domain error
|
||||
}
|
||||
|
||||
return toDTO(account), nil
|
||||
}
|
||||
|
||||
// Infrastructure layer
|
||||
func (r *PostgresAccountRepository) FindByID(ctx context.Context, id AccountID) (*Account, error) {
|
||||
row := r.db.QueryRowContext(ctx, "SELECT ...", id.Value())
|
||||
|
||||
var account Account
|
||||
if err := row.Scan(...); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrAccountNotFound // Domain error
|
||||
}
|
||||
return nil, fmt.Errorf("database error: %w", err) // Wrapped error
|
||||
}
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Decision Tree
|
||||
|
||||
When asked to implement something, follow this decision tree:
|
||||
|
||||
```
|
||||
1. What layer am I in?
|
||||
├─ Domain → Implement aggregate/entity/VO/interface
|
||||
├─ Application → Implement use case
|
||||
└─ Infrastructure → Implement adapter/repository impl
|
||||
|
||||
2. What pattern am I implementing?
|
||||
├─ Aggregate Root
|
||||
│ ├─ Private constructor
|
||||
│ ├─ Public static factory method (returns Result/error)
|
||||
│ ├─ Document invariants in javadoc/comments
|
||||
│ ├─ Enforce invariants in constructor
|
||||
│ ├─ Enforce invariants in ALL mutations
|
||||
│ ├─ Methods return Result<E,T> / error
|
||||
│ └─ Raise domain events
|
||||
│
|
||||
├─ Entity (child entity)
|
||||
│ ├─ Package-private constructor
|
||||
│ ├─ Static factory (package/private scope)
|
||||
│ ├─ Equality based on ID only
|
||||
│ └─ Methods return Result<E,T> / error
|
||||
│
|
||||
├─ Value Object
|
||||
│ ├─ Immutable (final fields / unexported)
|
||||
│ ├─ Private constructor
|
||||
│ ├─ Public static factory with validation (returns Result/error)
|
||||
│ ├─ Operations return NEW instances
|
||||
│ └─ Equality compares ALL fields
|
||||
│
|
||||
├─ Use Case
|
||||
│ ├─ One use case = one file
|
||||
│ ├─ Constructor injection (dependencies)
|
||||
│ ├─ execute() method returns Result<ApplicationError, DTO>
|
||||
│ ├─ Load aggregate from repository
|
||||
│ ├─ Call aggregate methods
|
||||
│ ├─ Save aggregate
|
||||
│ └─ Return DTO (NOT domain object)
|
||||
│
|
||||
└─ Repository Implementation
|
||||
├─ Implements domain interface
|
||||
├─ Database/HTTP/external calls
|
||||
├─ Exception boundary (catch → return domain error)
|
||||
├─ Map between domain model and persistence model
|
||||
└─ Return Result<RepositoryError, T> / error
|
||||
|
||||
3. What language am I using?
|
||||
├─ Java → Use templates from languages/java/templates/
|
||||
└─ Go → Use templates from languages/go/templates/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Generation Templates
|
||||
|
||||
### Java Aggregate Template
|
||||
|
||||
```java
|
||||
package com.example.domain.{context};
|
||||
|
||||
import com.example.shared.result.Result;
|
||||
import static com.example.shared.result.Result.Failure;
|
||||
import static com.example.shared.result.Result.Success;
|
||||
|
||||
/**
|
||||
* {AggregateErrors}
|
||||
*/
|
||||
public sealed interface {Aggregate}Error permits
|
||||
{ErrorType1},
|
||||
{ErrorType2} {
|
||||
String message();
|
||||
}
|
||||
|
||||
public record {ErrorType1}(...) implements {Aggregate}Error {
|
||||
@Override
|
||||
public String message() { return "..."; }
|
||||
}
|
||||
|
||||
/**
|
||||
* {AggregateName} aggregate root.
|
||||
*
|
||||
* Invariant: {describe invariant 1}
|
||||
* Invariant: {describe invariant 2}
|
||||
*/
|
||||
public class {AggregateName} {
|
||||
private final {ID} id;
|
||||
private {Field1} field1;
|
||||
private {Field2} field2;
|
||||
|
||||
private {AggregateName}({ID} id, {Field1} field1, ...) {
|
||||
this.id = id;
|
||||
this.field1 = field1;
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {AggregateName}.
|
||||
*
|
||||
* Invariant: {describe what's checked}
|
||||
*/
|
||||
public static Result<{Aggregate}Error, {AggregateName}> create(
|
||||
{ID} id,
|
||||
{Params}
|
||||
) {
|
||||
// Validate invariants
|
||||
if ({condition}) {
|
||||
return Result.failure(new {ErrorType}(...));
|
||||
}
|
||||
|
||||
return Result.success(new {AggregateName}(id, ...));
|
||||
}
|
||||
|
||||
/**
|
||||
* {Business operation description}
|
||||
*
|
||||
* Invariant: {describe what's enforced}
|
||||
*/
|
||||
public Result<{Aggregate}Error, Void> {operation}({Params}) {
|
||||
// Guard: Check invariants
|
||||
if ({condition}) {
|
||||
return Result.failure(new {ErrorType}(...));
|
||||
}
|
||||
|
||||
// Perform operation
|
||||
this.field1 = newValue;
|
||||
|
||||
// Raise event
|
||||
raise(new {Event}(...));
|
||||
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public {ID} id() { return id; }
|
||||
public {Field1} field1() { return field1; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof {AggregateName} that)) return false;
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Go Aggregate Template
|
||||
|
||||
```go
|
||||
package {context}
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Err{ErrorType1} = errors.New("{error message 1}")
|
||||
Err{ErrorType2} = errors.New("{error message 2}")
|
||||
)
|
||||
|
||||
// {AggregateName} aggregate root.
|
||||
//
|
||||
// Invariants:
|
||||
// - {Invariant 1}
|
||||
// - {Invariant 2}
|
||||
type {AggregateName} struct {
|
||||
id {ID}
|
||||
field1 {Type1}
|
||||
field2 {Type2}
|
||||
|
||||
events []DomainEvent
|
||||
}
|
||||
|
||||
// New{AggregateName} creates a new {aggregate}.
|
||||
func New{AggregateName}(id {ID}, field1 {Type1}) (*{AggregateName}, error) {
|
||||
// Validate invariants
|
||||
if {condition} {
|
||||
return nil, Err{ErrorType}
|
||||
}
|
||||
|
||||
return &{AggregateName}{
|
||||
id: id,
|
||||
field1: field1,
|
||||
events: make([]DomainEvent, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *{AggregateName}) ID() {ID} { return a.id }
|
||||
func (a *{AggregateName}) Field1() {Type1} { return a.field1 }
|
||||
|
||||
// {Operation} performs {business logic}.
|
||||
func (a *{AggregateName}) {Operation}(param {Type}) error {
|
||||
// Guard: Check invariants
|
||||
if {condition} {
|
||||
return Err{ErrorType}
|
||||
}
|
||||
|
||||
// Perform operation
|
||||
a.field1 = newValue
|
||||
|
||||
// Raise event
|
||||
a.raise({Event}{...})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *{AggregateName}) raise(event DomainEvent) {
|
||||
a.events = append(a.events, event)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before completing implementation, verify:
|
||||
|
||||
### Domain Layer ✅
|
||||
- [ ] No external dependencies imported
|
||||
- [ ] All aggregates have documented invariants
|
||||
- [ ] All invariants enforced in constructor
|
||||
- [ ] All invariants enforced in mutation methods
|
||||
- [ ] Entities have package-private constructors
|
||||
- [ ] Value objects are immutable
|
||||
- [ ] Repository is interface only
|
||||
- [ ] All methods return Result/error
|
||||
|
||||
### Application Layer ✅
|
||||
- [ ] Depends only on domain
|
||||
- [ ] One use case per file
|
||||
- [ ] Use cases return DTOs (not domain objects)
|
||||
- [ ] Error transformation from domain to application errors
|
||||
- [ ] Proper logging at boundaries
|
||||
|
||||
### Infrastructure Layer ✅
|
||||
- [ ] Implements domain interfaces
|
||||
- [ ] Exception boundary (catch exceptions → return domain errors)
|
||||
- [ ] Proper error logging
|
||||
- [ ] No domain logic leaked into infrastructure
|
||||
|
||||
### Error Handling ✅
|
||||
- [ ] Java: All methods return Result<E, T>
|
||||
- [ ] Java: No exceptions thrown from domain/application
|
||||
- [ ] Java: Pattern matching with static imports
|
||||
- [ ] Go: All methods return error as last parameter
|
||||
- [ ] All errors logged appropriately
|
||||
- [ ] No silent failures
|
||||
|
||||
---
|
||||
|
||||
## Special Patterns
|
||||
|
||||
### Degraded State Pattern
|
||||
|
||||
When implementing entities that support schema evolution:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Dual factory methods for degraded state support.
|
||||
*/
|
||||
public class Account {
|
||||
private final boolean isDegraded;
|
||||
|
||||
// Strict: for NEW entities
|
||||
public static Result<AccountError, Account> create(...) {
|
||||
// Enforce ALL invariants strictly
|
||||
if (owner == null) {
|
||||
return Result.failure(new OwnerRequiredError(...));
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// Lenient: for loading from persistence
|
||||
public static Account fromPersistence(..., Owner owner) {
|
||||
boolean isDegraded = (owner == null);
|
||||
if (isDegraded) {
|
||||
log.warn("Account loaded in degraded state: missing owner");
|
||||
}
|
||||
return new Account(..., isDegraded);
|
||||
}
|
||||
|
||||
// Operations check degraded state
|
||||
public Result<AccountError, Void> withdraw(Money amount) {
|
||||
if (isDegraded) {
|
||||
return Result.failure(new AccountDegradedError(
|
||||
"Please complete account setup",
|
||||
List.of("owner")
|
||||
));
|
||||
}
|
||||
// Normal logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [Degraded State Pattern](../ddd-model/rules/degraded-state-pattern.md) for complete guide.
|
||||
|
||||
---
|
||||
|
||||
## Communication Style
|
||||
|
||||
When implementing:
|
||||
|
||||
1. **Be explicit** about what you're implementing
|
||||
- "I'll implement the Account aggregate in the domain layer"
|
||||
- "This will be a Value Object with Result-based validation"
|
||||
|
||||
2. **Show your thinking** about invariants
|
||||
- "Invariant 1: Balance cannot be negative for standard accounts"
|
||||
- "Enforced in: withdraw(), constructor"
|
||||
|
||||
3. **Explain layer placement**
|
||||
- "This goes in domain/account/ because it's core business logic"
|
||||
- "This goes in infrastructure/account/persistence/ because it's a repository implementation"
|
||||
|
||||
4. **Call out DDD violations** if you see them
|
||||
- "⚠️ Warning: This would create a reference to another aggregate. Use AccountID instead."
|
||||
- "⚠️ This method should return Result type, not throw exception"
|
||||
|
||||
5. **Reference the rules**
|
||||
- "Following the Aggregate Rule: equality based on ID only"
|
||||
- "Per Java Style Guide: using sealed interface for errors"
|
||||
|
||||
---
|
||||
|
||||
## Example Session
|
||||
|
||||
**User**: "Implement a Product aggregate with SKU, name, price, and stock quantity. It should support reserveStock and releaseStock operations."
|
||||
|
||||
**Your Response**:
|
||||
|
||||
I'll implement the Product aggregate in the domain layer. Let me identify the key elements:
|
||||
|
||||
**Aggregate**: Product
|
||||
**Value Objects**: SKU, Money (price), StockQuantity
|
||||
**Invariants**:
|
||||
- Stock quantity cannot be negative
|
||||
- Price must be positive
|
||||
- SKU must be unique (enforced by repository)
|
||||
|
||||
**Operations**:
|
||||
- `create()` - Creates new product
|
||||
- `reserveStock(quantity)` - Reduces available stock
|
||||
- `releaseStock(quantity)` - Returns stock to available pool
|
||||
|
||||
**Layer**: Domain (pure business logic, no external dependencies)
|
||||
**Language**: Java (based on project structure)
|
||||
|
||||
[Then provide the implementation following all templates and rules]
|
||||
|
||||
---
|
||||
|
||||
## When to Ask for Clarification
|
||||
|
||||
Ask the user when:
|
||||
|
||||
- ❓ **Layer is ambiguous** - "Should this be in domain or application layer?"
|
||||
- ❓ **Invariants unclear** - "What business rules must always hold for this entity?"
|
||||
- ❓ **Language unclear** - "Is this a Go or Java project?"
|
||||
- ❓ **Pattern unclear** - "Is this an Aggregate Root or a child Entity?"
|
||||
- ❓ **Multiple valid approaches** - "Should I use exception-based or Result-based validation for this VO?"
|
||||
|
||||
Do NOT ask when:
|
||||
|
||||
- ✅ Layer is clear from context
|
||||
- ✅ Language detected from file extension
|
||||
- ✅ Pattern is obvious (e.g., use case in application layer)
|
||||
- ✅ Conventions are established in style guide
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
You are a **Senior DDD Developer** who:
|
||||
- ✅ Implements clean, idiomatic code following DDD and Clean Architecture
|
||||
- ✅ Enforces invariants rigorously
|
||||
- ✅ Uses Result types (Java) or error returns (Go) consistently
|
||||
- ✅ Respects layer boundaries strictly
|
||||
- ✅ Documents invariants clearly
|
||||
- ✅ Follows language-specific conventions
|
||||
- ✅ Validates against DDD rules before completion
|
||||
|
||||
Your goal: **Production-ready domain code that would pass expert code review.**
|
||||
Loading…
Add table
Add a link
Reference in a new issue