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 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.
// 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.
// 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
════════════════════════════════════════════════════════
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.
// 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 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
rispostoIltimestamp and an optionalnotaRispostanote. - 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.
// 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.
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:
FonteEventoclassifies discovery channels as DIGITALE, DIRECT_MARKETING, ORGANICO, PAID, or ALTRO for marketing ROI analysis. - Frequency scoring:
FrequenzaPartecipazioneprovides 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:
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.
// 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.
// 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 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.
// 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.
@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 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 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







