# 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)
    }
}
```
