The Domain Model: 86 Entities Powering Play The Event
Behind every feature of Play The Event lies a carefully designed domain model. With 86 entities organized into bounded contexts, the domain captures the full complexity of event management: from creating an event and managing participants, to tracking expenses and coordinating travel logistics.
This article explores the core aggregates, the event lifecycle state machine, participant roles and RSVP states, value objects that enforce business invariants, and the database migration strategy that keeps everything in sync.
What You'll Learn
- The core aggregates: Event, Trip, Expense, and User
- The event lifecycle: states, transitions, and business rules
- Participant roles and RSVP state management
- Value Objects: Money, Email, UserId, and more
- The expense and trip domains in detail
- Flyway migrations and schema evolution
Bounded Contexts Overview
The 86 entities are organized into bounded contexts, each representing a distinct area of the business domain. This separation ensures that each context has its own ubiquitous language and clear boundaries.
BOUNDED CONTEXTS (86 entities total)
EVENT CONTEXT (22 entities)
├── Event (Aggregate Root)
├── EventSettings, EventType, EventCategory
├── EventStatus, EventVisibility
├── EventLocation, EventSchedule
├── EventImage, EventDocument
├── EventTag, EventTemplate
└── Related configuration entities
PARTICIPANT CONTEXT (14 entities)
├── Participant (Aggregate Root)
├── ParticipantRole, ParticipantStatus
├── RsvpResponse, RsvpHistory
├── CheckIn, CheckOut
├── ParticipantPreferences
└── GuestInfo (+1 management)
EXPENSE CONTEXT (12 entities)
├── Expense (Aggregate Root)
├── ExpenseCategory, ExpenseSplit
├── Payment, PaymentStatus
├── Budget, BudgetItem
└── Currency, ExchangeRate
TRIP CONTEXT (10 entities)
├── Trip (Aggregate Root)
├── TripItinerary, TripLeg
├── Accommodation, Transportation
├── TripParticipant, TripDocument
└── TravelPreferences
USER CONTEXT (10 entities)
├── User (Aggregate Root)
├── UserProfile, UserPreferences
├── UserRole, UserPermission
├── AuthToken, PasswordReset
└── NotificationSettings
COMMUNICATION CONTEXT (8 entities)
├── Notification, NotificationTemplate
├── EmailLog, InAppMessage
└── AnnouncementBoard, Subscription
SURVEY CONTEXT (6 entities)
├── Questionnaire, Question
├── QuestionOption, Response
└── QuestionType, SurveyResult
SHARED KERNEL (4 entities)
├── AuditLog, SystemConfig
├── Language, Translation
└── (Shared across all contexts)
The Event Aggregate: The Core of the System
The Event aggregate is the central piece of the entire domain. It is the aggregate root that controls participants, settings, schedules, and all related entities.
Event Lifecycle State Machine
Every event in Play The Event follows a well-defined lifecycle. State transitions are enforced by the aggregate root, preventing invalid operations.
EVENT LIFECYCLE STATE MACHINE
┌──────────┐
│ DRAFT │ ← Initial state (event created)
└────┬─────┘
│ publish()
▼
┌──────────┐
│ PUBLISHED│ ← Visible, accepting RSVPs
└────┬─────┘
│ activate()
▼
┌──────────┐
│ ACTIVE │ ← Event is happening
└────┬─────┘
│ complete()
▼
┌──────────┐
│ COMPLETED│ ← Event finished, read-only
└──────────┘
CANCELLATION (from any non-completed state):
DRAFT / PUBLISHED / ACTIVE → cancel() → CANCELLED
RULES:
├── DRAFT → Only organizer can edit all fields
├── PUBLISHED → Participants can RSVP, limited edits
├── ACTIVE → Check-in/checkout enabled, no major edits
├── COMPLETED → Read-only, statistics available
└── CANCELLED → Notifications sent, data preserved
State Transition Guards
Each state transition has guards that must be satisfied. For example, an event cannot be published unless it has a name, date, and at least one organizer. An event cannot be activated before its start date. These guards are enforced in the domain layer, not in the controller.
Participant Roles and RSVP
Participants in Play The Event are not just "attendees". The system supports a rich role model that determines what each person can see and do within an event.
Participant Roles
Role Hierarchy
| Role | Permissions | Description |
|---|---|---|
| OWNER | Full control | Creator of the event, can delete it |
| ORGANIZER | Manage event, participants | Co-organizer with administrative access |
| MODERATOR | Manage participants, check-in | Handles on-site operations |
| PARTICIPANT | View event, RSVP, expenses | Standard attendee |
| GUEST | View public info only | Invited but limited access (+1) |
RSVP State Management
The RSVP system is more sophisticated than a simple yes/no. It tracks the history of responses and supports multiple states.
RSVP STATE MACHINE
┌──────────┐
│ PENDING │ ← Initial (invitation sent)
└────┬─────┘
│
├── accept() → CONFIRMED
├── decline() → DECLINED
└── maybe() → TENTATIVE
CONFIRMED ←→ DECLINED (can change mind)
CONFIRMED ←→ TENTATIVE (can change mind)
TENTATIVE ←→ DECLINED (can change mind)
ANY STATE → CANCELLED (event cancelled)
ADDITIONAL STATES:
├── WAITLISTED → Event at capacity, queued
├── CHECKED_IN → Physically present
└── NO_SHOW → Confirmed but did not attend
Value Objects
Value Objects enforce business rules at the type level. Instead of passing raw strings and numbers, the domain uses self-validating types that make invalid states unrepresentable.
Core Value Objects
VALUE OBJECTS IN PLAY THE EVENT
Money(amount: BigDecimal, currency: Currency)
├── Enforces non-negative amounts
├── Currency-safe arithmetic (add, subtract)
├── Prevents mixing different currencies
└── Used in: Budget, Expense, Payment
Email(value: String)
├── Validates format at construction
├── Normalized (lowercased, trimmed)
├── Immutable
└── Used in: User, Invitation, Notification
UserId(value: UUID)
├── Type-safe identifier
├── Prevents accidental ID mixing
├── Generated via UUID v4
└── Used in: all contexts referencing users
EventId(value: UUID)
├── Type-safe event identifier
├── Prevents passing wrong IDs
└── Used in: all event-related operations
DateRange(start: LocalDateTime, end: LocalDateTime)
├── Validates start before end
├── Overlap detection
├── Duration calculation
└── Used in: Event, Trip, Accommodation
Location(name: String, address: String,
latitude: Double, longitude: Double)
├── Validates coordinate ranges
├── Distance calculation
└── Used in: Event, TripLeg
Why Value Objects Matter
Without Value Objects, a method like transferMoney(BigDecimal, BigDecimal)
is ambiguous: which parameter is the amount and which is the fee? With Value Objects,
transferMoney(Money amount, Money fee) is self-documenting and type-safe.
The compiler catches mistakes that would otherwise become runtime bugs.
The Expense Domain
Expense management is one of the most complex bounded contexts in Play The Event. It handles budget planning, expense tracking, multiple splitting strategies, payment tracking, and multi-currency support.
EXPENSE AGGREGATE
Expense (Aggregate Root)
├── id: ExpenseId
├── event: EventId
├── description: String
├── amount: Money
├── category: ExpenseCategory
├── paidBy: UserId
├── splits: List<ExpenseSplit>
├── receipt: DocumentId (optional)
├── status: ExpenseStatus
└── createdAt: LocalDateTime
SPLITTING STRATEGIES (Strategy Pattern)
├── EQUAL → amount / participantCount
├── PERCENTAGE → amount * percentage / 100
├── EXACT → custom amounts per person
├── SHARES → amount * (shares / totalShares)
└── EXEMPTION → some participants excluded
BUDGET
├── totalBudget: Money
├── categories: List<BudgetItem>
├── spent: Money (calculated)
├── remaining: Money (calculated)
└── alerts: when spent > threshold
The Trip Domain
For events that involve travel, the Trip bounded context provides comprehensive logistics management, from itineraries and transportation to accommodation booking coordination.
TRIP AGGREGATE
Trip (Aggregate Root)
├── id: TripId
├── event: EventId
├── name: String
├── itinerary: TripItinerary
│ ├── legs: List<TripLeg>
│ │ ├── origin: Location
│ │ ├── destination: Location
│ │ ├── departure: LocalDateTime
│ │ ├── arrival: LocalDateTime
│ │ └── transportation: TransportType
│ └── totalDuration: Duration
├── participants: List<TripParticipant>
├── accommodations: List<Accommodation>
│ ├── name: String
│ ├── location: Location
│ ├── checkIn: LocalDate
│ ├── checkOut: LocalDate
│ └── cost: Money
└── documents: List<TripDocument>
Flyway Migrations
With 86 entities, managing database schema changes is critical. Play The Event uses Flyway for version-controlled migrations, ensuring that every environment (development, staging, production) has an identical schema.
FLYWAY MIGRATION FILES
db/migration/
├── V1_0__create_user_tables.sql
├── V1_1__create_event_tables.sql
├── V1_2__create_participant_tables.sql
├── V2_0__create_expense_tables.sql
├── V2_1__add_expense_splitting.sql
├── V3_0__create_trip_tables.sql
├── V3_1__add_accommodation.sql
├── V4_0__create_survey_tables.sql
├── V5_0__add_multilanguage_support.sql
├── V5_1__seed_languages.sql
│ ... (50+ migration files)
└── V12_3__add_checkin_checkout.sql
NAMING: V{major}_{minor}__{description}.sql
- Executed in order
- Never modified after deployment
- Rollback via compensating migrations
Migration Best Practices
In Play The Event, migrations follow strict rules: never alter a deployed migration, always create a new one. Additive changes (new columns, new tables) are preferred over destructive ones. Default values are always provided for new non-nullable columns to avoid breaking existing data.
Entity Relationships at a Glance
Understanding how the 86 entities relate to each other is essential. The following diagram shows the high-level relationships between the core aggregates.
ENTITY RELATIONSHIP MAP
User ──────┐
│ │
│ creates│ participates
▼ ▼
Event ◄── Participant
│ │
├── Trip ├── RsvpResponse
│ │ ├── CheckIn
│ ├── Accommodation
│ └── TripLeg
│
├── Expense
│ ├── ExpenseSplit → Participant
│ └── Payment
│
├── Document
├── Questionnaire
│ └── Response → Participant
│
└── Notification → Participant
CARDINALITIES:
├── User 1:N Event (as organizer)
├── User N:M Event (as participant)
├── Event 1:N Participant
├── Event 1:N Expense
├── Event 0:N Trip
├── Event 0:N Questionnaire
├── Expense 1:N ExpenseSplit
└── Trip 1:N TripLeg
In the Next Article
With the domain model fully explored, the next article shifts focus to a critical cross-cutting concern: security and JWT authentication. We will examine how Play The Event implements stateless authentication with HttpOnly cookies, role-based access control, and compliance with OWASP security guidelines.
Key Takeaways
- 86 entities organized into 8 bounded contexts with clear boundaries
- The Event aggregate root enforces a state machine with guarded transitions
- Participants have a rich role model (Owner, Organizer, Moderator, Participant, Guest)
- RSVP supports 7 states including waitlisting, check-in, and no-show tracking
- Value Objects (Money, Email, UserId) enforce business rules at the type level
- Flyway manages 50+ migrations with strict versioning discipline







