Organizations and Festivals: Scaling Beyond Single Events
When a single user account is no longer enough, organizations step in. In Play The Event, an organization can be either a company (Azienda) or an association (Associazione), each with its own profile, members, and role hierarchy. And when an organization needs to manage multi-day events with rooms, exhibition stands, and detailed schedules, the Festival module takes over.
This article explores the two sub-domains that allow Play The Event to scale from managing a single event to orchestrating full-blown multi-day festivals with dozens of participants and stakeholders.
What You Will Learn in This Article
- The two organization types: Company and Association profiles
- Membership and hierarchical roles with bidirectional workflow
- Email invitations for users not yet registered on the platform
- Festival as an Aggregate Root for multi-day events
- Festival days, rooms, stands, and their orchestration
- Stand booking system with a complete lifecycle
- The Money Value Object for budget management
Two Types of Organization
The platform supports four account types via the TipoAccount enum: INDIVIDUO (individual), AZIENDA (company), ASSOCIAZIONE (association), and SUPER_AMMINISTRATORE (super admin). The first three are selectable during registration. When a user selects AZIENDA or ASSOCIAZIONE, the system requires an additional profile to be filled out.
TipoAccount
├── INDIVIDUO → No additional profile needed
├── AZIENDA → Requires ProfiloAzienda
├── ASSOCIAZIONE → Requires ProfiloAssociazione
└── SUPER_AMMINISTRATORE → Not selectable during registration
ProfiloAzienda (Company Profile)
A company has a rich profile with international tax data: business name (ragioneSociale), country (ISO 3166-1 alpha-2 code), VAT Number, Tax ID, and company registration number. It also includes contact details (business email, PEC for Italian companies), the legal address via the Indirizzo Value Object, a contact person, and the business sector.
@Entity
public class ProfiloAzienda {
private Long id;
private User user; // 1:1 relationship
// Identification
private String ragioneSociale; // "TechCorp S.r.l."
private String paese; // "IT" (ISO 3166-1 alpha-2)
private String vatNumber; // "IT12345678901"
private String taxId; // Codice Fiscale (Tax ID)
private String companyRegistrationNumber;
// Contacts
private String emailAziendale; // Business email
private String pec; // Certified email (Italian companies)
// Address and contact person
private Indirizzo sedeLegale; // Embedded Value Object
private String referenteNome; // Contact first name
private String referenteCognome; // Contact last name
private String referenteRuolo; // Contact role
private SettoreAzienda settore; // Business sector
}
ProfiloAssociazione (Association Profile)
Associations have a different profile, tailored to the non-profit world. The key field is TipoAssociazione, which varies by country: Italy supports APS, ASD, ODV, ETS, ONLUS; the USA supports 501(c)(3); the UK supports Charity. Each type is validated against the selected country to prevent invalid combinations.
@Entity
public class ProfiloAssociazione {
private Long id;
private User user; // 1:1 relationship
private String nomeAssociazione; // "ASD Runners Bari"
private String paese; // "IT"
private TipoAssociazione tipoAssociazione; // APS, ASD, ODV, 501C3...
private String registrationNumber; // RUNTS, EIN, etc.
private String taxId;
private Indirizzo sede; // Address
private String rappresentanteLegaleNome; // Legal representative
private String rappresentanteLegaleCognome;
}
// Country-type validation:
// TipoAssociazione.ASD.isValidoPerPaese("IT") → true
// TipoAssociazione.ASD.isValidoPerPaese("US") → false
// TipoAssociazione._501C3.isValidoPerPaese("US") → true
Key Differences Between the Two Profiles
- ProfiloAzienda: full tax data (VAT, PEC, company registration), contact person, business sector, BUSINESS subscription plans
- ProfiloAssociazione: country-validated association type, legal representative, registration number (RUNTS/EIN), ASSOCIATION plans with 35-38% discount
Membership and Roles: AppartenenzaOrganizzazione
A user with an AZIENDA or ASSOCIAZIONE account becomes an organization that other users can belong to. The relationship between organization and member is modeled by the AppartenenzaOrganizzazione entity, which serves as the Aggregate Root for the membership sub-domain.
Hierarchical Roles
Roles are defined by the RuoloOrganizzazione enum with four increasing authorization levels.
public enum RuoloOrganizzazione {
MEMBRO(10), // Member: participates and receives plan benefits
RESPONSABILE(50), // Manager: can create events/trips for the org
ADMIN(80), // Administrator: manages members and roles
OWNER(100); // Owner: full control (only one per org)
}
PERMISSIONS:
MEMBRO RESPONSABILE ADMIN OWNER
Receive benefits ✓ ✓ ✓ ✓
Create events ✗ ✓ ✓ ✓
Manage members ✗ ✗ ✓ ✓
Change roles ✗ ✗ ✓ ✓
Invite members ✗ ✗ ✓ ✓
Remove members ✗ ✗ ✓ ✓
The OWNER role is special: it is automatically assigned to
the organization creator, cannot be assigned via invitation, and cannot be
suspended. Transferring ownership requires a dedicated
trasferisciOwnership() method.
Bidirectional Workflow
Membership supports two flows: the organization invites a member (initial state PENDING_INVITO) or a member requests to join (initial state PENDING_RICHIESTA). In both cases, the recipient can accept or decline.
public enum StatoAppartenenza {
PENDING_INVITO, // Org invited, waiting for member's response
PENDING_RICHIESTA, // Member requested, waiting for org's approval
ATTIVO, // Active member of the organization
SOSPESO, // Temporarily suspended
RIFIUTATO // Invitation/request declined
}
INVITATION FLOW (from organization):
PENDING_INVITO ──accept──► ATTIVO ──suspend──► SOSPESO
│ │
└──decline──► RIFIUTATO reactivate ◄──┘
REQUEST FLOW (from member):
PENDING_RICHIESTA ──approve──► ATTIVO
│
└──decline──► RIFIUTATO
The system supports multi-membership: a user can belong to multiple organizations simultaneously, with different roles in each. A unique constraint on the pair (organizzazione_id, membro_id) prevents duplicate memberships.
Email Invitations: InvitoOrganizzazioneEmail
How do you invite someone who is not yet registered on the platform? Through InvitoOrganizzazioneEmail. This entity manages the entire flow: from sending an email with a unique token to converting the invitation into an active AppartenenzaOrganizzazione when the user accepts.
1. Admin/Owner creates invite with recipient email
2. System generates SecureRandom token (48 bytes, Base64 URL-safe)
3. Email sent with link containing the token
4. Recipient clicks the link
├── If registered → Accepts directly
└── If not registered → Registers first, then accepts
5. Invitation converted to active AppartenenzaOrganizzazione
INVITATION STATES:
PENDING ──accept──► ACCETTATO
│
├──cancel──► ANNULLATO (by the org)
├──expire──► SCADUTO (after N days, default 7)
└──renew───► PENDING (new token, new expiry)
Invitation Security
Each invitation generates a cryptographically secure 48-byte token via SecureRandom. The token has a configurable expiry (default 7 days) and is regenerated when the invitation is renewed. The email address is normalized (lowercase, trimmed) and validated before sending.
Festival: The Aggregate Root for Multi-Day Events
Let us now turn to the second major topic. A Festival in Play The Event is a complex aggregate that models multi-day events with scheduled days, equipped rooms, exhibition stands, and a structured program. Following DDD principles, Festival is the Aggregate Root with a strong consistency boundary: all modifications flow through it.
Festival (Aggregate Root)
├── id, titolo, descrizione
├── organizzatoreId // Who organizes it
├── luogoId // Main location
├── dataInizio / dataFine // Start / end dates
├── stato: StatoFestival // Lifecycle state
├── budget: Money // Embedded Value Object
├── maxPartecipantiTotali // Max total participants
├── tokenCondivisione // QR / link sharing
│
├── giornate: Set<GiornataFestival>
│ └── each day contains its EventoFestival entries
│
├── sale: Set<SalaFestival>
│ └── each room has its ConfigurazioneSala entries
│
└── stand: Set<StandFestival>
└── each stand has its PrenotazioneStand entries
Festival Lifecycle
A festival goes through five well-defined states, with controlled transitions and validations at every step.
public enum StatoFestival {
DRAFT, // Draft: in preparation, not publicly visible
PUBLISHED, // Published: visible, waiting to begin
IN_CORSO, // In progress: currently running
COMPLETATO, // Completed: successfully concluded
ANNULLATO // Cancelled
}
TRANSITIONS:
DRAFT ──publish──► PUBLISHED ──start──► IN_CORSO ──complete──► COMPLETATO
│ │
└──cancel──► ANNULLATO ◄──cancel───┘
▲
DRAFT ───────────────cancel────────────┘
PERMISSIONS BY STATE:
DRAFT PUBLISHED IN_CORSO COMPLETATO ANNULLATO
Modifiable ✓ ✓ ✗ ✗ ✗
Accepts events ✓ ✓ ✓ ✗ ✗
Accepts bookings ✓ ✓ ✗ ✗ ✗
Cancellable ✓ ✓ ✓ ✗ ✗
Publishing a festival requires at least one configured day. The
pubblica() method checks this prerequisite and throws an
exception if it is not met.
Festival Days: GiornataFestival
Each GiornataFestival represents a single day within the festival. It has a date, an optional title, start and end times (defaulting to 09:00 - 23:00), and a sequential order number.
@Entity
public class GiornataFestival {
private Long id;
private Long festivalId;
private LocalDate data;
private String titolo; // "Day 1 - Opening"
private String descrizione;
private LocalTime oraInizio; // Default: 09:00
private LocalTime oraFine; // Default: 23:00
private Integer ordine; // Position in calendar
private Set<EventoFestival> eventi; // Scheduled events
}
VALIDATIONS:
- Date must fall between the festival's dataInizio and dataFine
- No two days can share the same date
- Event times must fall within the day's time boundaries
- A day with scheduled events cannot be removed
The Festival also offers a generaGiornate() method that
automatically creates a GiornataFestival for every day in the festival
period, skipping any dates that already have a corresponding day entry.
Rooms: SalaFestival and Configurations
SalaFestival entities represent the physical spaces within the festival venue. Each room has a type that determines its capabilities and intended use.
public enum TipoSala {
SALA_CONFERENZE // Conference room (seated, events)
AUDITORIUM // Large hall for many attendees (seated)
PALCO // Stage for shows and performances (events)
AREA_ESTERNA // Outdoor area (events)
STAND_AREA // Exhibition stand area
SALA_WORKSHOP // Workshop room (seated, events)
FOYER // Welcome and networking area
SALA_STAMPA // Press room for journalists (seated, events)
}
CAPABILITIES:
Seated Capacity Hosts Events
SALA_CONFERENZE ✓ ✓
AUDITORIUM ✓ ✗
PALCO ✗ ✓
AREA_ESTERNA ✗ ✓
STAND_AREA ✗ ✗
SALA_WORKSHOP ✓ ✓
FOYER ✗ ✗
SALA_STAMPA ✓ ✓
ConfigurazioneSala (Room Configurations)
A room can have multiple configurations with different capacities. For instance, a conference room might be set up in "Theater" mode (200 seated), "Auditorium" mode (150 seated + 50 standing), or "Standing" mode (300 standing). Configurations can be individually activated or deactivated.
SalaFestival: "Sala Magna" (SALA_CONFERENZE)
├── Configuration "Teatro" (Theater)
│ ├── Seated: 200
│ ├── Standing: 0
│ └── Active: true
├── Configuration "Platea" (Auditorium)
│ ├── Seated: 150
│ ├── Standing: 50
│ └── Active: true
└── Configuration "Standing"
├── Seated: 0
├── Standing: 300
└── Active: false
Max capacity: max(200, 200, 300) = 300 (among active configs: 200)
Stands: StandFestival and Bookings
StandFestival entities represent exhibition spaces. The fundamental distinction is between PROPRIO stands (managed by the organization itself) and AFFITTABILE stands (rentable to third parties at a daily price).
StandFestival
├── codice: "A01" // Unique identifier
├── nome: "Main Stand"
├── tipo: PROPRIO | AFFITTABILE
├── dimensioniMq: 25.50 // Size in square meters
├── prezzoGiornaliero: Money // Daily price (AFFITTABILE only)
├── dotazioni: "Table, chairs, power outlet"
├── posizioneMappa: "Hall A, Row 1"
│
└── prenotazioni: Set<PrenotazioneStand>
└── PrenotazioneStand
├── giornataId // Which festival day
├── affittuarioNome // Who is renting
├── affittuarioEmail
├── attivitaNome // What they will exhibit
├── stato: StatoPrenotazione
└── importoPagato: Money // Payment tracking
Booking Lifecycle
Each booking follows a simple but effective lifecycle. A stand can be booked for a specific festival day, with a uniqueness constraint: no two active bookings can exist for the same stand on the same day.
public enum StatoPrenotazione {
PRENOTATO, // Booked: awaiting confirmation
CONFERMATO, // Confirmed: active booking
ANNULLATO // Cancelled
}
TRANSITIONS:
PRENOTATO ──confirm──► CONFERMATO
│ │
└──cancel──► ANNULLATO ◄┘
CONSTRAINTS:
- Only AFFITTABILE stands can be booked
- One tenant per stand per day
- Bookings include payment tracking via Money
EventoFestival: Linking Events to Days and Rooms
The EventoFestival entity is the bridge between an event and the festival context. It links an event to a specific day, with a precise time slot and an optional room assignment. The system automatically checks for overlaps within the same room.
public enum TipoEventoFestival {
// Main stage events
MAIN_EVENT, KEYNOTE, CERIMONIA, CONCERTO, SPETTACOLO
// Educational events
TALK, WORKSHOP, PANEL
// Networking and services
NETWORKING, REGISTRAZIONE, BREAK, ALTRO
}
EXAMPLE SCHEDULE - Day 1:
09:00-09:30 REGISTRAZIONE (check-in) (Foyer)
09:30-10:30 CERIMONIA opening (Auditorium)
10:30-11:00 BREAK (Foyer)
11:00-12:00 KEYNOTE speaker (Auditorium)
11:00-12:30 WORKSHOP Angular (Workshop Room A)
11:00-12:30 WORKSHOP Spring Boot (Workshop Room B)
12:30-14:00 BREAK lunch
14:00-14:45 TALK microservices (Conference Room 1)
14:00-14:45 TALK frontend (Conference Room 2)
15:00-16:30 PANEL "The future of the web" (Auditorium)
17:00-18:00 NETWORKING (Outdoor Area)
21:00-23:00 CONCERTO (Stage)
Overlap Detection
When an event is assigned to a room, the system verifies that there are
no time overlaps with other events in the same room. Two events overlap if
A.oraInizio < B.oraFine AND A.oraFine > B.oraInizio.
Events without a room assignment are not checked for overlaps.
Budget Management with the Money Value Object
Both the Festival and its stands use the Money Value Object
to handle monetary amounts. Money is an immutable object that encapsulates an
amount (BigDecimal with scale 2 and HALF_UP rounding) and a
currency (String following ISO 4217).
@Embeddable
public class Money implements Comparable<Money> {
private BigDecimal amount; // Scale 2, HALF_UP rounding
private String currency; // ISO 4217 (EUR, USD, GBP...)
// Factory methods
Money.of(100.00, "EUR")
Money.euro(50.00)
Money.zero("EUR")
// Arithmetic (same currency required)
money1.add(money2) // Addition
money1.subtract(money2) // Subtraction
money1.multiply(1.22) // Multiplication (e.g., tax)
money1.divide(3) // Division
// Queries
money.isPositive()
money.isZero()
money.isGreaterThan(other)
}
USAGE IN FESTIVAL:
Festival.budget = Money.euro(50000.00) // Total budget
StandFestival.prezzoGiornaliero = Money.euro(150.00) // Daily rental price
PrenotazioneStand.importoPagato = Money.euro(450.00) // 3 days x 150
Using Money as a Value Object ensures that amounts in different currencies cannot be added together (the system throws an exception) and that all rounding is consistent across the application.
Festival Sharing
Every festival can generate a sharing token (8 alphanumeric characters) that allows quick sharing via link or QR code. The organizer can activate or deactivate sharing at any time.
Key Takeaways
- Two organization profiles (Company and Association) with country-specific validation
- AppartenenzaOrganizzazione with 4 hierarchical roles and bidirectional invite/request workflow
- InvitoOrganizzazioneEmail for inviting unregistered users with secure tokens and expiry
- Festival as a DDD Aggregate Root with 5 lifecycle states
- GiornataFestival with configurable hours and automatic day generation
- 8 room types with multiple configurations and capacity management
- PROPRIO and AFFITTABILE stands with per-day booking system
- 12 festival event types with per-room overlap detection
- Money Value Object for type-safe budget and payment handling
The source code is available on GitHub. To explore organization and festival management in action, visit www.playtheevent.com.







