Skip to main content
The Hub platform uses Domain-Driven Design (DDD) principles to model the business domain. This page documents the domain model, including aggregates, entities, value objects, and their relationships.

Domain-Driven Design Concepts

Aggregates

Clusters of domain objects treated as a single unit (e.g., Booking, Venue)

Entities

Objects with unique identity that persists over time

Value Objects

Immutable objects defined by their attributes (e.g., Address, Money)
All domain entities are located in modules/*/domain/ directories

Booking Domain

Booking Aggregate

The Booking aggregate manages the complete lifecycle of a court booking.
Location: modules/booking/domain/Booking.javaAttributes:
public class Booking {
    private final BookingId id;                // Unique identifier
    private final ResourceId resourceId;       // Court being booked
    private final UserId playerId;            // Player making booking
    private final LocalDate bookingDate;      // Date of booking
    private final SlotRange slot;             // Time slot (start/end)
    private final BigDecimal pricePaid;       // Amount paid
    private final String currency;            // Currency code (EUR)
    private BookingStatus status;             // Booking lifecycle state
    private PaymentStatus paymentStatus;      // Payment state
    private Instant cancelledAt;              // Cancellation timestamp
    private String cancelReason;              // Reason for cancellation
    private final Instant createdAt;          // Creation timestamp
    private Instant updatedAt;                // Last update timestamp
    private Instant expiresAt;                // Payment hold expiration
}
Factory Methods:
// Regular player booking
Booking.create(
    resourceId, playerId, bookingDate, slot, 
    pricePaid, currency, holdDuration, clock
)

// Match organizer booking (different lifecycle)
Booking.createForMatch(
    resourceId, organizerId, bookingDate, slot,
    price, currency, expiresAt, clock
)

// Reconstitution from persistence
Booking.reconstitute(...) // All fields from database
Domain Methods:
  • confirmPayment(clock) - Mark payment as complete, activate booking
  • expireHold(clock) - Cancel booking if payment not received
  • cancel(reason, clock) - User cancellation (24h rule enforced)
  • adminCancel(reason, clock) - Admin override cancellation
  • confirmMatch(clock) - Confirm match booking when full
  • revertToPendingMatch(clock) - Revert to pending if player leaves
  • cancelMatch(clock) - Cancel match booking
Business Invariants:
  • Payment must be confirmed before booking activation
  • Cancellations within 24h of start time are not allowed (user)
  • Admin cancellations bypass time restrictions
  • Match bookings follow different state transitions

Payment Entity

Location: modules/booking/domain/Payment.javaPurpose: Tracks payment transactions linked to bookingsKey Attributes:
  • Payment intent ID (payment provider integration)
  • Amount and currency
  • Payment status tracking
  • Link to booking and player

Booking Value Objects

public record BookingId(UUID value) {
    public static BookingId generate() {
        return new BookingId(UUID.randomUUID());
    }
    
    public static BookingId from(String value) {
        return new BookingId(UUID.fromString(value));
    }
}

Venue Domain

Venue Aggregate

Location: modules/venue/domain/Venue.javaAttributes:
public class Venue {
    private final VenueId id;
    private final UserId ownerId;             // Venue owner
    private VenueName name;
    private String description;
    private Address address;                  // Value object
    private Coordinates coordinates;          // For geo search
    private final List<VenueImage> images;   // Venue photos
    private VenueStatus status;              // Approval state
    private String rejectReason;             // Admin feedback
    private final Instant createdAt;
    private Instant updatedAt;
}
Factory Methods:
Venue.create(ownerId, name, description, address, coordinates, clock)
Venue.reconstitute(...) // From database
Domain Methods:
  • update(...) - Modify venue details (triggers re-review if active)
  • suspend(clock) - Owner-initiated suspension
  • reactivate(clock) - Resume after suspension
  • addImage(imageUrl, clock) - Add venue photo
  • removeImage(imageId, clock) - Delete photo
  • approve(clock) - Admin approval
  • reject(reason, clock) - Admin rejection
  • adminSuspend(reason, clock) - Admin suspension
Business Invariants:
  • Only owner can modify venue
  • Updates to active venues trigger PENDING_REVIEW status
  • At least one image recommended (not enforced)
  • Coordinates required for geographic search

VenueImage Entity

Purpose: Manages venue photos with display ordering Attributes:
  • Image URL and public ID (Cloudinary)
  • Display order (integer)
  • Creation timestamp

Venue Value Objects

public record VenueId(UUID value) {
    public static VenueId generate() {
        return new VenueId(UUID.randomUUID());
    }
}

Resource Domain

Resource Aggregate

Location: modules/resource/domain/Resource.javaAttributes:
public class Resource {
    private final ResourceId id;
    private final VenueId venueId;                      // Parent venue
    private ResourceName name;
    private String description;
    private ResourceType type;                          // Court type
    private SlotDuration slotDuration;                 // Booking increment
    private final Map<DayOfWeek, DaySchedule> schedules; // Operating hours
    private final List<PriceRule> priceRules;          // Dynamic pricing
    private final List<ResourceImage> images;          // Court photos
    private ResourceStatus status;
    private String rejectReason;
    private final Instant createdAt;
    private Instant updatedAt;
}
Factory Methods:
Resource.create(venueId, name, description, type, slotDuration, clock)
Resource.reconstitute(...) // From database
Domain Methods:
  • update(...) - Modify resource details
  • setSchedule(day, opening, closing, clock) - Set hours for day
  • removeSchedule(day, clock) - Remove day availability
  • addPriceRule(...) - Add pricing rule
  • removePriceRule(id, clock) - Delete pricing rule
  • addImage(imageUrl, clock) - Add photo
  • removeImage(imageId, clock) - Delete photo
  • suspend/reactivate(clock) - Owner suspension
  • approve/reject/adminSuspend(...) - Admin actions
  • generateSlotsForDay(day) - Generate bookable slots
  • getPriceForSlot(day, time) - Calculate slot price
  • isAvailableOn(day) - Check day availability
Business Invariants:
  • Resource belongs to exactly one venue
  • Schedules define operating hours per day of week
  • Price rules evaluated in order (most specific wins)
  • Slot duration divides evenly into schedule

DaySchedule Entity

Purpose: Operating hours for a specific day of week Attributes:
  • Day of week
  • Opening time
  • Closing time
Methods:
  • generateSlots(slotDuration) - Create bookable time slots

Resource Value Objects

public enum ResourceType {
    PADEL_COURT,
    TENNIS_COURT,
    SQUASH_COURT,
    BADMINTON_COURT,
    MULTI_SPORT_COURT
}

Matching Domain

MatchRequest Aggregate

Location: modules/matching/domain/MatchRequest.javaAttributes:
public class MatchRequest {
    private final MatchRequestId id;
    private final UserId organizerId;              // Match creator
    private final ResourceId resourceId;           // Court booked
    private final LocalDate bookingDate;
    private final LocalTime startTime;
    private final int slotDurationMinutes;
    private final MatchFormat format;              // Singles/Doubles
    private final MatchSkillLevel skillLevel;     // Skill requirement
    private final String customMessage;            // Organizer message
    private final InvitationToken invitationToken; // Public join link
    private final GeoPoint searchCenter;          // Search location
    private final double searchRadiusKm;          // Search radius
    private final BigDecimal pricePerPlayer;      // Cost per player
    private final Instant expiresAt;              // Auto-close time
    private final Instant createdAt;
    
    private MatchStatus status;
    private final List<MatchPlayer> players;      // Participants
}
Factory Method:
MatchRequest.create(
    organizerId, resourceId, bookingDate, startTime,
    slotDurationMinutes, format, skillLevel, customMessage,
    searchCenter, searchRadiusKm, pricePerPlayer, clock
)
Domain Methods:
  • openForPlayers() - Open after organizer payment
  • cancelDueToPaymentTimeout() - Cancel if organizer doesn’t pay
  • join(playerId, team, clock) - Player joins match
  • cancel() - Organizer cancellation
  • removePlayer(playerId) - Remove player
  • checkIn(playerId, clock) - Mark player present
  • reportAbsence(playerId) - Report no-show
  • expire() - Auto-close after expiration time
  • availableSlots() - Calculate remaining spots
Business Invariants:
  • Organizer must pay before match opens
  • Player count cannot exceed format max
  • Teams must be balanced (doubles)
  • Match expires 24h before start time
  • Players cannot join if already in match

MatchPlayer Entity

Purpose: Represents a player’s participation in a match Attributes:
  • Player ID
  • Team assignment (A or B)
  • Role (ORGANIZER or GUEST)
  • Joined timestamp
  • Check-in status
  • Absence reporting

MatchInvitation Entity

Purpose: Tracks invitations sent to eligible players Attributes:
  • Match request ID
  • Player ID and email
  • Invitation status (PENDING, ACCEPTED, DECLINED)
  • Sent and responded timestamps
  • Free substitute flag

Matching Value Objects

public enum MatchFormat {
    SINGLES(2, 1),    // 2 players, 1 per team
    DOUBLES(4, 2);    // 4 players, 2 per team
    
    private final int maxPlayers;
    private final int playersPerTeam;
    
    public int getMaxPlayers() { return maxPlayers; }
    public int getPlayersPerTeam() { return playersPerTeam; }
}

IAM Domain

UserProfile Aggregate

Location: modules/iam/domain/UserProfile.javaAttributes:
public class UserProfile {
    private final UserId id;
    private final Auth0Id auth0Id;              // External auth provider ID
    private Email email;
    private boolean emailVerified;
    private DisplayName displayName;
    private String description;
    private PhoneNumber phoneNumber;
    private ImageUrl avatar;
    private UserRole role;                      // PLAYER, OWNER, ADMIN
    private OwnerRequestStatus ownerRequestStatus;
    private SportPreference preferredSport;
    private SkillLevel skillLevel;
    private String city;
    private CountryCode countryCode;
    private boolean active;
    private boolean onboardingCompleted;
    private int noShowCount;                    // Match reliability
    private Instant matchBannedUntil;          // Temporary ban
    private Instant lastMatchCancelledAt;
    private boolean matchNotificationsEnabled;
    private final Instant createdAt;
    private Instant updatedAt;
    private Instant lastLoginAt;
}
Factory Method:
UserProfile.create(auth0Id, clock)  // Auto-created on first login
Domain Methods:
  • updateProfile(...) - Edit profile information
  • updateAvatar(imageUrl, clock) - Change profile picture
  • requestOwnerRole(clock) - Request venue owner access
  • approveOwnerRequest(clock) - Admin approval
  • rejectOwnerRequest(clock) - Admin rejection
  • changeRole(newRole, clock) - Admin role change
  • toggleActive(clock) - Admin ban/unban
  • confirmNoShow(clock) - Record match absence
  • isMatchBanned(clock) - Check if temporarily banned
  • recordLogin(clock) - Update last login timestamp
Business Invariants:
  • Auth0 ID is unique and immutable
  • Default role is PLAYER
  • Owner role requires admin approval
  • 3 no-shows trigger 30-day ban
  • Onboarding completed when name and city set

IAM Value Objects

public record Auth0Id(String value) {
    public Auth0Id {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Auth0 ID required");
        }
    }
}

Shared Value Objects

package com.ccasro.hub.shared.domain.valueobjects;

public record UserId(UUID value) {
    public static UserId newId() {
        return new UserId(UUID.randomUUID());
    }
    
    public static UserId from(String value) {
        return new UserId(UUID.fromString(value));
    }
}

Domain Events

Domain events represent important business occurrences:
public record BookingConfirmedEvent(
    BookingId bookingId,
    UserId playerId,
    ResourceId resourceId,
    LocalDate bookingDate,
    SlotRange slot,
    Instant occurredAt
) {}

public record BookingCancelledEvent(
    BookingId bookingId,
    UserId playerId,
    String reason,
    Instant occurredAt
) {}

public record BookingExpiredEvent(
    BookingId bookingId,
    Instant occurredAt
) {}
public record MatchFullEvent(
    MatchRequestId matchRequestId,
    UserId organizerId,
    List<UserId> playerIds,
    Instant occurredAt
) {}

public record MatchInvitationsEvent(
    MatchRequestId matchRequestId,
    List<UserId> invitedPlayerIds,
    Instant occurredAt
) {}

Aggregate Boundaries

Aggregates are consistency boundaries - changes within an aggregate are atomic.
Key Aggregates:
  1. Booking - Manages booking lifecycle and payment
  2. Venue - Groups venue details and images
  3. Resource - Groups court, schedules, pricing, and images
  4. MatchRequest - Groups match details and players
  5. UserProfile - User account and preferences
Relationships:
  • Aggregates reference each other by ID only
  • Cross-aggregate operations coordinated by use cases
  • No direct object references between aggregates

Value Object Patterns

Immutability

All value objects are immutable (using Java records):
public record Address(String street, String city, String country, String postalCode) {
    // Compact constructor validates invariants
    public Address {
        if (street == null || street.isBlank()) {
            throw new IllegalArgumentException("Street required");
        }
        // Normalize values
        street = street.trim();
        city = city.trim();
    }
}

Self-Validation

Value objects validate themselves on construction:
public record Email(String value) {
    public Email {
        if (value == null || !value.matches(EMAIL_REGEX)) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

Identity vs Value

Entities (identity matters):
  • Booking, Venue, Resource, UserProfile
  • Compared by ID
  • Mutable state
Value Objects (value matters):
  • Address, Coordinates, SlotRange
  • Compared by attributes
  • Immutable

Backend Modules

Module structure and architecture

Database Schema

Persistence mapping and database structure

Build docs developers (and LLMs) love