Skip to main content

05 - A Complex Domain: 86 Entities for Real Problems

A Complex Domain for Real Problems The domain model is the heart of any application that adopts Domain-Driven Design. Discover how to model 86 entities with aggregates, value objects, and state machines for a real event management system.

What do you think about this article?

At a Glance

A Complex Domain for Real Problems The domain model is the heart of any application that adopts Domain-Driven Design. Discover how to model 86 entities with aggregates, value objects, and state machines for a real event management system.

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 and Entity Distribution
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 State Machine
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 States and Transitions
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

Key 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 Domain Model
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 Domain Structure
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.

Migration Naming Convention
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.

Core Aggregate Relationships
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

Frequently asked questions

What is the purpose of bounded contexts in the domain model?

Bounded contexts represent distinct areas of the business domain, ensuring each context has its own ubiquitous language and clear boundaries.

How does the Event aggregate root control related entities in the system?

The Event aggregate root controls participants, settings, schedules, and all related entities by enforcing state transitions and preventing invalid operations.

What is the significance of value objects in the domain model?

Value objects enforce business rules at the type level, making invalid states unrepresentable and reducing runtime bugs by catching mistakes with the compiler.

How does Play The Event manage database schema changes across different environments?

Play The Event uses Flyway for version-controlled migrations, ensuring every environment has an identical schema through strict migration rules.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects