Skip to main content

14 - Organizations and Festivals: Multi-Level Management

Organizations and Festivals: Multi-Level Management When a single user is no longer enough, organizations come into play.

What do you think about this article?

At a Glance

Organizations and Festivals: Multi-Level Management When a single user is no longer enough, organizations come into play.

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 and Profiles
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.

ProfiloAzienda Structure
@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.

ProfiloAssociazione Structure
@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.

Role Hierarchy
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.

Membership Lifecycle
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.

Email Invitation Workflow
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 Structure
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.

StatoFestival
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.

GiornataFestival
@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.

Room Types (TipoSala)
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.

Example: Room Configurations
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).

Stand and Booking System
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.

StatoPrenotazione (Booking State)
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.

Festival Event Types (TipoEventoFestival)
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).

Money Value Object
@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.

Frequently asked questions

What are the two types of organizations supported by Play The Event?

A company (AZIENDA) and an association (ASSOCIAZIONE), each with its own profile, members, and role hierarchy.

How does a user invite someone who is not yet registered on the platform to join their organization?

Through the InvitoOrganizzazioneEmail entity, which manages the entire invitation flow from sending an email with a unique token to converting the invitation into an active membership when the user accepts.

What are the five well-defined states that a festival goes through in Play The Event?

Publishing, Configured, Scheduled, Running, and Finished, each with controlled transitions and validations at every step.

How does a festival generate a sharing token to allow quick sharing via link or QR code?

By generating an 8-character alphanumeric token that can be activated or deactivated by the organizer at any time.

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