Skip to main content

07 - Event and Participant Management: The Platform Core

The Central Module: Event and Participant Management. The core of Play The Event platform handles event lifecycle, participant registration, RSVP, and check-in workflows.

What do you think about this article?

At a Glance

The Central Module: Event and Participant Management. The core of Play The Event platform handles event lifecycle, participant registration, RSVP, and check-in workflows.

Event and Participant Management: The Core of Play The Event

The event management module is the central nervous system of Play The Event. Every other module in the platform, from expenses to trips, from documents to surveys, orbits around the Event aggregate and its participants. Understanding how events are created, transition through states, and how participants interact with them is essential to understanding the entire system.

In this article, we dive deep into the Evento aggregate root, the participant role system, the RSVP workflow, the check-in and check-out mechanisms, and the REST API that exposes it all. Every design decision reflects real-world requirements collected from organizers of birthday parties, corporate retreats, association galas, and community meetups.

What You'll Find in This Article

  • The complete event lifecycle: from DRAFT to COMPLETED (and CANCELLED)
  • Participant roles: ORGANIZER, CO_ORGANIZER, ATTENDEE, and VIP
  • The RSVP system with six distinct states and invitation expiry
  • External guest check-in: collecting data from unregistered attendees
  • Check-in and check-out with token-based public forms
  • Event visibility and public sharing via secure tokens
  • Capacity management and real-time participant counting
  • REST API endpoints for events and participants

The Event Lifecycle

An event in Play The Event is not a static record in a database. It is a living entity that moves through a well-defined state machine. Each state determines what actions are allowed, what the organizer can modify, and whether new participants can join.

The StatoEvento enum defines nine possible states. The primary flow follows a linear progression, but events can also be cancelled or postponed from most active states.

Event State Machine
EVENT LIFECYCLE - STATE TRANSITIONS

DRAFT ──────> PUBLISHED ──────> CONFIRMED ──────> IN_PROGRESS
  │               │                                    │
  │               ├──> INVITATIONS_SENT ──> CONFIRMED  │
  │               │                                    │
  │               v                                    v
  │           POSTPONED                        EXPENSE_SPLITTING
  │                                                    │
  v                                                    v
CANCELLED <── (from any active state)            COMPLETED


State Descriptions:
─────────────────────────────────────────────────────────
DRAFT              Event is being planned, not yet visible
PUBLISHED          Event is live, invitations can be sent
INVITATIONS_SENT   Invitations dispatched, awaiting RSVPs
CONFIRMED          Event is confirmed, will take place
IN_PROGRESS        Event is currently happening
EXPENSE_SPLITTING  Event ended, expenses being divided
COMPLETED          All done, expenses settled
CANCELLED          Event was cancelled
POSTPONED          Event postponed to a future date

State Transition Rules

Not every transition is allowed. The domain model enforces strict rules about which state changes are valid, preventing illogical flows like completing an event that hasn't started or publishing an event without a title and date.

State Transition Guards (StatoEvento.java)
// Which states allow new participants to join?
canAcceptParticipants():
  DRAFT, PUBLISHED, INVITATIONS_SENT, CONFIRMED  ->  true
  IN_PROGRESS, EXPENSE_SPLITTING, COMPLETED, CANCELLED  ->  false

// Which states allow event details to be modified?
isModifiable():
  DRAFT, PUBLISHED, INVITATIONS_SENT  ->  true
  All other states  ->  false

// When can expenses be split?
canSplitExpenses():
  IN_PROGRESS, EXPENSE_SPLITTING  ->  true
  All other states  ->  false

// When can an event be marked complete?
canComplete():
  EXPENSE_SPLITTING  ->  true  (expenses must be divided first)
  All other states  ->  false

The Expense Splitting Gate

A critical design decision: an event cannot be completed without first passing through the EXPENSE_SPLITTING state. This means every event that has expenses must have them divided among participants before it can be archived. This prevents the common problem of forgotten debts and unresolved payments that plague informal event organization.

Publishing Validation

When an organizer attempts to publish a draft event, the aggregate validates that the minimum required information is present. An event without a title or a start date cannot be published. This is enforced at the domain level, not at the controller level, following DDD principles.

Publication Guard
// Domain validation before publishing
validaEventoPuoEsserePubblicato():
  - Title must not be blank
  - Start date (dataInizio) must be set

// Only DRAFT events can be published
pubblica():
  if (stato != DRAFT) throw IllegalStateException
  validaEventoPuoEsserePubblicato()
  stato = PUBLISHED

Participant Roles

Every participant in an event has a role that determines their permissions and capabilities. The RuoloPartecipante enum defines four distinct roles, each with a clear responsibility boundary.

Participant Roles and Permissions
PARTICIPANT ROLES
════════════════════════════════════════════════════════

ORGANIZER (Organizzatore)
  - Created the event
  - Full modification permissions
  - Can add/remove co-organizers
  - Can promote participants to CO_ORGANIZER
  - Cannot be removed from the event
  - RSVP automatically set to ACCEPTED
  - Plus-one automatically allowed

CO_ORGANIZER (Co-Organizzatore)
  - Can modify event details
  - Can invite new participants
  - Cannot delete the event
  - Cannot add other co-organizers
  - RSVP automatically set to ACCEPTED
  - Plus-one automatically allowed

ATTENDEE (Partecipante)
  - Standard participant
  - Read-only access to event details
  - Can respond to RSVP
  - Can manage own additional info

VIP (Partecipante VIP)
  - Special status participant
  - Same permissions as ATTENDEE
  - Visual distinction in participant lists

Permission Matrix

The role system provides three key permission checks used throughout the application to gate actions:

Role Permission Matrix

Permission ORGANIZER CO_ORGANIZER ATTENDEE VIP
Modify event Yes Yes No No
Invite participants Yes Yes No No
Add co-organizers Yes No No No
Delete event Yes No No No
Change states Yes Yes No No
Auto-accepted RSVP Yes Yes No No
Auto plus-one Yes Yes No No

Role Promotion and Demotion

The organizer can promote any ATTENDEE or VIP to CO_ORGANIZER, and can also demote a CO_ORGANIZER back to ATTENDEE. These operations are performed through the Evento aggregate root, which enforces the business rules. When a participant is promoted to a co-organizer role, the system automatically enables the plus-one privilege.

Role Change Rules
// Promotion to CO_ORGANIZER
promuoviACoorganizzatore(userId):
  - User must be an existing participant
  - User must NOT already be ORGANIZER or CO_ORGANIZER
  - Changes role to CO_ORGANIZER
  - Automatically enables plus-one privilege

// Demotion from CO_ORGANIZER
retroceidiCoorganizzatore(userId):
  - User must be CO_ORGANIZER (not ORGANIZER)
  - Changes role back to ATTENDEE

// ORGANIZER is immutable
  - Cannot be removed from the event
  - Cannot change their own role
  - Only one ORGANIZER per event

The RSVP System

The RSVP (Repondez s'il vous plait) system in Play The Event manages the invitation-response lifecycle for each participant. The StatoRSVP enum defines six states that cover every possible scenario, from fresh invitations to expired deadlines.

RSVP States
RSVP STATE MACHINE
════════════════════════════════════════════════════════

           ┌──────────> ACCETTATO (Accepted)
           │
IN_ATTESA ─┼──────────> RIFIUTATO (Declined)
(Pending)  │
           ├──────────> FORSE (Maybe)
           │
           └──────────> SCADUTO (Expired)
                          (automatic, via scheduled job)

ACCETTATO ────────────> ANNULLATO (Cancelled)
                          (participant withdraws)

FORSE ────────────────> ACCETTATO | RIFIUTATO | ANNULLATO
                          (participant changes mind)

RSVP Business Rules

The domain model enforces several invariants around RSVP responses that prevent inconsistent states:

  • Organizers auto-accept: When a participant is created with the ORGANIZER or CO_ORGANIZER role, their RSVP is automatically set to ACCETTATO. They cannot decline.
  • Cancelled invitations are final: Once an RSVP is in the ANNULLATO state, the participant cannot accept the invitation again.
  • Organizers cannot decline: An organizer attempting to decline their own invitation triggers an IllegalStateException.
  • Response tracking: Every RSVP change records a rispostoIl timestamp and an optional notaRisposta note.
  • Invitation tracking: Each participant records who invited them (invitatoDaUtenteId) and when (invitatoIl).

Invitation Expiry

Organizers can set a deadline for RSVP responses using the dataScadenzaInvito field. A scheduled job checks for expired invitations and transitions them from IN_ATTESA to SCADUTO. Only pending invitations can expire; if a participant has already responded, the expiry date has no effect.

Invitation Expiry Logic
// Check if invitation has expired
isInvitoScaduto():
  if dataScadenzaInvito == null -> false (no deadline set)
  if statoRsvp != IN_ATTESA -> false (already responded)
  if now > dataScadenzaInvito -> true (deadline passed)

// Scheduled job marks expired invitations
marcaComeScaduto():
  Precondition: statoRsvp == IN_ATTESA
  Sets statoRsvp = SCADUTO
  Publishes InvitoScadutoEvent

RSVP Counting Methods

The Event aggregate provides methods to count participants based on their RSVP status, enabling organizers to get an accurate picture of expected attendance:

Participant Counting

Method Counts Used For
contaPartecipantiConfermati() ACCETTATO only Capacity checks, confirmed headcount
contaPartecipantiPotenziali() ACCETTATO + FORSE Optimistic planning, catering estimates
getPartecipantiInAttesa() IN_ATTESA only Pending responses, follow-up reminders

External Guest Management

Not every person at an event has a Play The Event account. For concerts, open-air festivals, and public gatherings, the platform provides a dedicated check-in system for external guests (CheckinInvitato) that collects demographic data and survey responses from walk-in attendees.

CheckinInvitato Entity - Data Collected
EXTERNAL GUEST CHECK-IN DATA
════════════════════════════════════════════════════════

Personal Information:
  - nome (first name)          VARCHAR(100)
  - cognome (last name)        VARCHAR(100)
  - eta (age)                  INT (1-120)
  - cittaProvenienza (city)    VARCHAR(100)
  - statoProvenienza (country) VARCHAR(100)
  - email (optional)           VARCHAR(255)

Survey Data:
  - frequenzaPartecipazione    How often they attend similar events
      PRIMA_VOLTA              First time
      UNA_DUE_VOLTE            1-2 times before
      TRE_CINQUE_VOLTE         3-5 times before
      PIU_DI_CINQUE            More than 5 times

  - fonteEvento                How they discovered the event
      SOCIAL_MEDIA             Facebook, Instagram, etc.
      AMICI_PASSAPAROLA        Friends / word of mouth
      EMAIL_NEWSLETTER         Email or newsletter
      PUBBLICITA               Advertising
      SITO_WEB                 Website
      ALTRO                    Other

  - accompagnatori             Who they came with
      SOLO                     Alone
      CON_UN_AMICO             With 1 friend
      CON_DUE_TRE_AMICI        With 2-3 friends
      CON_PIU_DI_TRE           With more than 3 friends
      CON_FAMIGLIA             With family

Auto-generated:
  - orarioArrivo               Arrival timestamp (Instant.now())
  - iscrizioneNewsletter       Newsletter opt-in (requires email)

Analytics and ML Features

The CheckinInvitato entity is designed with analytics and machine learning in mind. It provides helper methods that categorize guests for downstream processing:

  • Age brackets: getFasciaEta() groups guests into ranges (UNDER_18, 18_24, 25_34, 35_44, 45_54, 55_64, 65_PLUS) for demographic analysis.
  • Group size estimation: getNumeroStimatoGruppo() calculates the average between minimum and maximum group sizes based on the companion type.
  • Source categorization: FonteEvento classifies discovery channels as DIGITALE, DIRECT_MARKETING, ORGANICO, PAID, or ALTRO for marketing ROI analysis.
  • Frequency scoring: FrequenzaPartecipazione provides numeric values (0-3) for ML model input.

Check-in and Check-out

Play The Event implements a token-based access control system for both check-in (arrival) and check-out (post-event feedback). Each event can generate unique, secure tokens that enable public-facing forms without requiring authentication.

Token Architecture

The Evento aggregate manages three independent token systems, each with its own enable/disable flag:

Three Token Systems
TOKEN SYSTEMS IN EVENTO
════════════════════════════════════════════════════════

1. SHARING TOKEN (tokenCondivisione)
   Purpose: Public event page link
   Length:  8 characters (alphanumeric, SecureRandom)
   Flag:    condivisioneAttiva (boolean)
   URL:     /public/events/share/{token}

2. CHECK-IN TOKEN (tokenCheckin)
   Purpose: Guest arrival form
   Length:  8 characters (alphanumeric, SecureRandom)
   Flag:    checkinAttivo (boolean)
   URL:     /public/events/checkin/{token}

3. CHECK-OUT TOKEN (tokenCheckout)
   Purpose: Post-event feedback form
   Length:  8 characters (alphanumeric, SecureRandom)
   Flag:    checkoutAttivo (boolean)
   URL:     /public/events/checkout/{token}

Each token can be:
  - Generated (first time)
  - Regenerated (invalidates previous link)
  - Activated / Deactivated (without deleting the token)

Check-in Flow for Registered Participants

Registered participants who have accepted the invitation can perform a check-in that records their physical arrival at the event. This is different from the external guest check-in and is tracked directly on the Partecipante entity.

Registered Participant Check-in
// Check-in for registered participants
effettuaCheckIn():
  Precondition: statoRsvp == ACCETTATO
  Precondition: registrato == false (not already checked in)

  Sets registrato = true
  Sets registratoIl = Instant.now()
  Publishes CheckInEffettuatoEvent

// Fields on Partecipante entity:
  registrato     BOOLEAN   Has the participant checked in?
  registratoIl   TIMESTAMP When did they check in?

Two Check-in Systems

It is important to distinguish between the two check-in systems: registered participant check-in (a boolean flag on the Partecipante entity for known users) and external guest check-in (a separate CheckinInvitato entity for walk-in guests without accounts). Both serve the same goal, tracking who is physically present, but they address different user segments.

Event Visibility and Public Sharing

Event visibility on Play The Event is controlled through multiple mechanisms that give organizers fine-grained control over who can see their event.

Sharing Token

The sharing token enables a public-facing event page accessible via a unique URL. This is the LINK_ONLY visibility model: the event is not listed publicly, but anyone with the link can view its details.

Sharing Token Operations
// Generate and activate sharing
generaTokenCondivisione():
  tokenCondivisione = SecureRandom(8 chars, A-Za-z0-9)
  condivisioneAttiva = true
  return token

// Toggle sharing on/off (preserves existing token)
attivaCondivisione():
  if no token exists -> generate new one
  condivisioneAttiva = true

disattivaCondivisione():
  condivisioneAttiva = false
  (token is kept for potential reactivation)

// Regenerate (invalidates old link)
rigeneraTokenCondivisione():
  generates a completely new token
  old links stop working immediately

Map Visibility

Events with geographic coordinates can optionally appear on a public map. This is controlled by the visibileInMappa flag, which is independent of the sharing token. An event can be shared via link without appearing on the map, and vice versa.

Visibility Combinations
VISIBILITY MATRIX
════════════════════════════════════════════════════════

Sharing  | Map     | Behavior
─────────┼─────────┼─────────────────────────────────
  OFF    |  OFF    | Fully private (participants only)
  ON     |  OFF    | Accessible via link, not on map
  OFF    |  ON     | Visible on public map, no link
  ON     |  ON     | Both link sharing and map visible

Capacity and Real-Time Management

The maxPartecipanti field on the Evento aggregate enables capacity management. When set, the system enforces a hard limit on confirmed participants, preventing overbooking.

Capacity Management Logic
// Check if seats are available
haPostiDisponibili():
  if maxPartecipanti == null -> true (no limit set)
  return contaPartecipantiConfermati() < maxPartecipanti

// Get remaining seats
getPostiRimanenti():
  if maxPartecipanti == null -> null (unlimited)
  return maxPartecipanti - contaPartecipantiConfermati()

// Enforced when adding participants
aggiungiPartecipante(userId, role):
  if maxPartecipanti != null AND
     contaPartecipantiConfermati() >= maxPartecipanti
  -> throw "Maximum participants reached"

The capacity check counts only confirmed participants (RSVP = ACCETTATO). Participants with FORSE (maybe) status do not count against the limit. This design choice allows the organizer to over-invite when some responses are uncertain, relying on the confirmed count for hard capacity enforcement.

Additional Participant Information

Beyond roles and RSVP, each participant record can store additional details relevant to event logistics:

  • Dietary restrictions (restrizioniAlimentari): Free-text field for allergies, preferences, or special requirements.
  • Plus-one (plusOneConsentito, nomePlusOne): Organizers can allow specific participants to bring a guest. The guest's name is stored for check-in coordination.
  • Invitation deadline (dataScadenzaInvito): Per-participant expiry date for RSVP responses.

The Evento Aggregate Root

Following Domain-Driven Design principles, Evento is the aggregate root of the event bounded context. All modifications to participants, state transitions, and visibility settings flow through the Evento entity, which enforces consistency boundaries.

Evento Entity - Key Fields
@Entity
@Table(name = "eventi")
public class Evento {

    // Identity
    Long id
    String titolo              // max 200 chars
    String descrizione         // TEXT, max 2000 chars

    // State
    StatoEvento stato          // DRAFT, PUBLISHED, ... COMPLETED
    Long organizzatoreId       // Creator's user ID

    // Schedule
    Instant dataInizio         // Start date/time
    Instant dataFine           // End date/time

    // Budget
    Money budget               // Embedded Value Object (amount + currency)
    Valuta valuta              // Currency entity reference
    TipoRipartizioneSpese tipoRipartizione  // Expense split type

    // Location
    String luogo               // Venue name (max 500 chars)
    String luogoLink           // Venue URL
    Double latitudine          // GPS latitude (-90 to 90)
    Double longitudine         // GPS longitude (-180 to 180)

    // Capacity
    Integer maxPartecipanti    // Optional participant limit

    // Tokens
    String tokenCondivisione   // 8-char sharing token
    String tokenCheckin        // 8-char check-in token
    String tokenCheckout       // 8-char check-out token

    // Flags
    Boolean condivisioneAttiva
    Boolean checkinAttivo
    Boolean checkoutAttivo
    Boolean visibileInMappa

    // Audit
    Instant creatoIl           // @CreatedDate
    Instant aggiornatoIl       // @LastModifiedDate
    Long versione              // @Version (optimistic locking)

    // Relationships
    Set<Partecipante> partecipanti  // OneToMany, lazy loaded
}

Note the use of optimistic locking via the @Version annotation. When two organizers attempt to modify the same event simultaneously, the second write will fail with an OptimisticLockException, preventing data corruption in concurrent scenarios.

REST API

The event and participant functionality is exposed through two primary REST controllers, following RESTful conventions with sub-resource routing for participants.

Event API Endpoints
EVENT CONTROLLER - /api/v1/events
════════════════════════════════════════════════════════

CRUD Operations:
  POST   /api/v1/events                    Create new event
  GET    /api/v1/events/{id}               Get event by ID
  GET    /api/v1/events                    Get user's events (paginated)
  GET    /api/v1/events/all                Get all events
  PUT    /api/v1/events/{id}               Update event details
  DELETE /api/v1/events/{id}               Delete event

State Transitions:
  POST   /api/v1/events/{id}/publish                Publish event
  POST   /api/v1/events/{id}/confirm                Confirm event
  POST   /api/v1/events/{id}/start                  Start event
  POST   /api/v1/events/{id}/start-expense-splitting Start expense phase
  POST   /api/v1/events/{id}/complete               Complete event
  POST   /api/v1/events/{id}/cancel                 Cancel event

Sharing:
  POST   /api/v1/events/{id}/share                  Enable sharing
  GET    /api/v1/events/{id}/share                  Get share status
  DELETE /api/v1/events/{id}/share                  Disable sharing
  POST   /api/v1/events/{id}/share/regenerate       Regenerate token

Check-in Management:
  POST   /api/v1/events/{id}/checkin/enable         Enable check-in
  DELETE /api/v1/events/{id}/checkin/disable         Disable check-in
  POST   /api/v1/events/{id}/checkin/regenerate     Regenerate token
  GET    /api/v1/events/{id}/checkin/status          Get check-in status
  GET    /api/v1/events/{id}/checkin/list            List checked-in guests
  GET    /api/v1/events/{id}/checkin/stats           Get check-in stats

Check-out Management:
  POST   /api/v1/events/{id}/checkout/enable        Enable check-out
  DELETE /api/v1/events/{id}/checkout/disable        Disable check-out
  POST   /api/v1/events/{id}/checkout/regenerate    Regenerate token
  GET    /api/v1/events/{id}/checkout/status         Get check-out status
  GET    /api/v1/events/{id}/checkout/list           List feedback entries
  GET    /api/v1/events/{id}/checkout/stats          Get check-out stats

Statistics:
  GET    /api/v1/events/statistiche                 User event statistics
Participant API Endpoints
PARTICIPANT CONTROLLER - /api/v1/events/{eventId}/participants
════════════════════════════════════════════════════════

  GET    /...participants                 List all participants
  GET    /...participants/{participantId} Get participant details
  POST   /...participants                 Add new participant
  DELETE /...participants/{participantId} Remove participant
  PATCH  /...participants/{participantId}/role  Change participant role

API Design Principles

The API follows several important conventions: participants are modeled as sub-resources of events (nested routing), state transitions use POST to action endpoints rather than PATCH on a status field, and all endpoints are documented with OpenAPI/Swagger annotations for automatic API documentation generation.

In the Next Article

With a solid understanding of the event lifecycle and participant management, the next article will explore the expense tracking and splitting system: how Play The Event handles budgets, records individual expenses, and fairly divides costs among participants using multiple splitting strategies.

Key Takeaways

  • Events follow a strict state machine with 9 states, enforcing business rules at the domain level
  • The EXPENSE_SPLITTING gate ensures expenses are always settled before an event is completed
  • Four participant roles (ORGANIZER, CO_ORGANIZER, ATTENDEE, VIP) with clear permission boundaries
  • Six RSVP states with invitation expiry, auto-accept for organizers, and response tracking
  • Dual check-in system: registered participants (boolean flag) and external guests (full entity)
  • Token-based public forms for sharing, check-in, and check-out with enable/disable toggles
  • Capacity management counts only confirmed participants, allowing optimistic over-inviting
  • 30+ REST endpoints cover the full event and participant lifecycle

Frequently asked questions

What is the primary state machine that an event in Play The Event moves through?

The event has a well-defined state machine with nine possible states, which determines what actions are allowed and what the organizer can modify.

How does the domain model enforce rules about state transitions for events?

The domain model enforces strict rules about which state changes are valid, preventing illogical flows like completing an event that hasn't started or publishing an event without a title and date.

What is the purpose of the role system in Play The Event?

Every participant in an event has a role that determines their permissions and capabilities, with four distinct roles each having clear responsibility boundaries.

How does the platform handle external guests who do not have a Play The Event account?

The platform provides a dedicated check-in system for external guests that collects demographic data and survey responses from walk-in attendees.

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