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