Two Complementary Systems for Collective Decision-Making
Organizing an event means making dozens of decisions, and the best decisions are often the ones that involve everyone. Where should the company offsite be held? Which restaurant for the rehearsal dinner? How satisfied were attendees with the conference sessions?
In Play The Event, we built two distinct but complementary systems to address these needs: Polls (Sondaggi) for structured voting with predefined options, and Surveys (Questionari) for open-ended feedback collection with typed questions. Together, they give organizers a complete toolkit for gathering participant input before, during, and after any event.
What You Will Find in This Article
- The Poll system with its state machine and lifecycle management
- Single and multiple response types with configurable selection limits
- Anonymous voting and public polls with UUID-based sharing
- Reusable templates for instantiation across different events
- Winner history tracking with mandatory motivation and audit trail
- Entity-linked polls for venue voting with rich option metadata
- The Survey system with typed questions and structured compilations
- Question types: open text, single choice, and multiple choice
- Real-time result visualization and aggregation
The Poll System: Structured Voting
A poll in Play The Event
is modeled as an Aggregate Root that encapsulates the entire voting lifecycle.
The Sondaggio entity owns its options, responses, winner history, and
configuration, ensuring that all business rules are enforced at the domain level rather
than scattered across services.
Every poll is linked to an event and created by a specific user. It carries a title, an optional description, a response type, and a rich set of configurable behaviors that control how participants interact with it.
POLL (Sondaggio) - Aggregate Root
├── id: Long (auto-generated)
├── evento: Evento (the parent event)
├── tipoEntita: TipoEntitaSondaggio (LUOGO, DATA, MENU, ATTIVITA, CUSTOM)
├── titolo: String (required)
├── descrizione: String (optional, TEXT)
├── etichetta: String (max 50 chars, uppercase)
├── tipoRisposta: TipoRispostaSondaggio (SINGOLA | MULTIPLA)
├── stato: StatoSondaggio (BOZZA | ATTIVO | CHIUSO | ANNULLATO)
├── tipo: TipoSondaggio (TEMPLATE | EVENTO)
├── opzioni: Set<OpzioneSondaggio> (ordered by ordine ASC)
├── anonimo: Boolean (default: false)
├── permettiModificaVoto: Boolean (default: true)
├── mostraRisultatiPrimaVoto: Boolean (default: false)
├── maxOpzioniSelezionabili: Integer (default: 1 for SINGOLA, 3 for MULTIPLA)
├── dataScadenza: LocalDateTime (optional deadline)
├── pubblico: Boolean (default: false)
├── codiceCondivisione: String (UUID, unique)
├── creatoDa: User (required)
├── creatoIl: Instant (auto-set)
└── aggiornatoIl: Instant (auto-updated)
The Poll State Machine
The lifecycle of a poll follows a strict state machine with four states and well-defined transitions. Each transition is guarded by preconditions that prevent invalid operations, and the state machine is enforced entirely within the domain entity.
STATE MACHINE: StatoSondaggio
pubblica() chiudi()
BOZZA ──────────► ATTIVO ──────────► CHIUSO
(Draft) (Active) (Closed)
│ ▲ │
│ │ riapri() │
│ └────────────────┘
│
│ annulla()
└──────────► ANNULLATO
(Cancelled)
TRANSITION RULES:
├── BOZZA → ATTIVO: requires at least 2 options
├── ATTIVO → CHIUSO: only active polls can be closed
├── ATTIVO → ANNULLATO: cancellation from any non-cancelled state
├── CHIUSO → ATTIVO: reopening restores voting capability
└── BOZZA is the only modifiable state (options, config)
A critical design decision is that only polls in the BOZZA (Draft) state are modifiable. Once a poll is published and becomes ATTIVO, its structure is locked. Options cannot be added or removed, and configuration changes are blocked. This guarantees that no participant votes on a poll that changes after they have cast their ballot.
Reopening: A Deliberate Choice
The ability to reopen a closed poll (CHIUSO → ATTIVO) is intentionally
supported. Real-world scenarios frequently require extending a voting period: perhaps
not enough participants voted, or new options need to be considered. However, reopening
does not allow structural changes to the poll. It simply re-enables the voting window.
The transition from ANNULLATO is a one-way street; cancelled polls cannot be reopened.
Response Types: Single and Multiple Choice
Every poll is configured with one of two response types, defined by the
TipoRispostaSondaggio enum. This choice fundamentally affects how participants
interact with the poll and how results are aggregated.
SINGOLA (Single Choice)
In single choice mode, each participant can select exactly one option. This is the classic
radio button pattern, ideal for binary decisions or mutually exclusive choices. When a
poll is created with SINGOLA, the maxOpzioniSelezionabili is
automatically set to 1.
MULTIPLA (Multiple Choice)
Multiple choice mode allows participants to select more than one option. The maximum
number of selectable options is configurable through maxOpzioniSelezionabili,
which defaults to 3 but can be set to any positive integer. This enables scenarios like
selecting the top three preferred restaurants or voting for multiple activity options.
SINGLE CHOICE (SINGOLA)
├── Max selectable: 1 (enforced)
├── UI pattern: Radio buttons
├── Use case: "Which restaurant for dinner?"
│ ├── ○ La Trattoria
│ ├── ● Chez Pierre ← selected
│ └── ○ Sakura Sushi
└── Result: One clear winner
MULTIPLE CHOICE (MULTIPLA)
├── Max selectable: configurable (default: 3)
├── UI pattern: Checkboxes
├── Use case: "Which activities interest you? (pick up to 3)"
│ ├── ☑ Kayaking
│ ├── ☐ Wine tasting
│ ├── ☑ City tour
│ ├── ☑ Cooking class
│ └── ☐ Museum visit
└── Result: Ranked by total votes per option
The maxOpzioniSelezionabili Guard
- For SINGOLA polls, the value is always 1 regardless of configuration
- For MULTIPLA polls, the value defaults to 3 but can be customized
- The minimum allowed value is 1; setting it to 0 throws an IllegalArgumentException
- Validation is enforced at the domain level during the
configura()method
Anonymous Voting
Polls support an anonimo flag that controls whether voter identities are
recorded alongside their responses. When anonymous mode is enabled, the system still
tracks that a vote was cast (to prevent duplicate voting), but the association between
the voter and their chosen option is not exposed to other participants or organizers
when viewing results.
This feature is particularly important for sensitive topics where honest opinions matter more than attribution, such as choosing between competing proposals from different team members or rating the performance of event organizers.
Public Polls with UUID Sharing
By default, polls are private and only accessible to authenticated event participants.
However, any active poll can be made public through the rendiPubblico()
method, which generates a unique UUID-based sharing code.
PUBLIC POLL FLOW
STEP 1: Organizer creates and publishes poll
poll.pubblica() → stato: ATTIVO
STEP 2: Organizer makes poll public
poll.rendiPubblico()
→ pubblico: true
→ codiceCondivisione: "a3f8b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
STEP 3: Share link is generated
→ https://playtheevent.com/poll/a3f8b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c
STEP 4: External users vote without authentication
→ VotoPubblico tracks fingerprint to prevent duplicates
→ RispostaSondaggio.creaAnonima() for fully anonymous votes
STEP 5: Organizer can revoke or regenerate
poll.rendiPrivato() → disables public access (keeps code)
poll.rigeneraCodiceCondivisione() → invalidates old links
The VotoPubblico entity plays a crucial role in public polls. It tracks
anonymous votes using a browser fingerprint, preventing the same device from voting
multiple times on the same poll. The unique constraint on
(sondaggio_id, fingerprint) ensures this at the database level.
Security Model for Public Polls
Public polls deliberately trade authentication for reach. The fingerprint-based
duplicate prevention is a pragmatic compromise: it stops casual double-voting without
requiring users to create accounts. The organizer can revoke public access at any time
by calling rendiPrivato(), and regenerating the sharing code invalidates
all previously distributed links without affecting votes already cast.
Reusable Templates
Organizations that run recurring events often need the same polls with minor variations.
Play The Event addresses this with a template system built directly into the
Sondaggio entity through the TipoSondaggio enum.
A template is a poll that is not linked to any event. It serves as a blueprint that
can be instantiated for specific events through the creaDaTemplate() factory
method, which performs a deep copy of all configuration and options while resetting the
state to BOZZA.
TEMPLATE SYSTEM
STEP 1: Create a reusable template
Sondaggio.creaTemplate(user, "Venue Preference", LUOGO, title, SINGOLA)
→ tipo: TEMPLATE
→ evento: null
→ Add options: "Beach Resort", "Mountain Lodge", "City Hotel"
STEP 2: Instantiate for Event A
Sondaggio.creaDaTemplate(template, eventoA, user)
→ tipo: EVENTO
→ evento: Event A
→ templateOrigineId: template.id
→ stato: BOZZA (always starts fresh)
→ Options: copied from template (new IDs, no votes)
STEP 3: Instantiate for Event B
Sondaggio.creaDaTemplate(template, eventoB, user)
→ Same template, different event
→ Independent lifecycle, independent votes
FIELDS COPIED FROM TEMPLATE:
├── tipoEntita, titolo, descrizione, etichetta
├── tipoRisposta, anonimo, permettiModificaVoto
├── mostraRisultatiPrimaVoto, maxOpzioniSelezionabili
└── Options (via copiaOpzioniDa - separate step)
FIELDS NOT COPIED:
├── stato (always BOZZA)
├── pubblico (always false)
├── votes and responses
└── winner selection
Winner History and Audit Trail
For entity-linked polls, particularly those of type LUOGO (Venue), the system
supports formal winner selection after the poll closes. This is not simply picking the
option with the most votes; it is a deliberate organizer decision that gets recorded with
full audit capabilities through the StoricoVincitoreSondaggio entity.
First Selection
When an organizer selects a winner for the first time on a closed LUOGO poll, a history
record is created via creaPrimaSelezione(). The motivation field is optional
for the initial selection, as the choice typically reflects the poll results.
Changing the Winner
When the winner needs to change after the initial selection, the system enforces a mandatory motivation. The organizer must provide a textual explanation for why the winner is being changed. This creates accountability and prevents arbitrary overrides of democratic voting results.
WINNER HISTORY: StoricoVincitoreSondaggio
RECORD 1: Initial Selection
├── opzionePrecedente: null
├── opzioneNuova: "Beach Resort" (ID: 42)
├── motivazione: null (optional for first selection)
├── modificatoDa: "Alice" (organizer)
└── modificatoIl: 2026-03-15T10:30:00Z
RECORD 2: Winner Change
├── opzionePrecedente: "Beach Resort" (ID: 42)
├── opzioneNuova: "Mountain Lodge" (ID: 43)
├── motivazione: "Beach resort fully booked for our dates,
│ mountain lodge has better group rates"
├── modificatoDa: "Alice" (organizer)
└── modificatoIl: 2026-03-16T14:15:00Z
BUSINESS RULES:
├── Poll must be CHIUSO (closed) to select winner
├── Poll must be of type LUOGO (venue)
├── Motivation is MANDATORY for changes (not first selection)
├── New option must differ from previous (validated)
├── Full history is preserved and ordered by date DESC
└── Cannot select option not belonging to this poll
Why Mandatory Motivation Matters
- Accountability: Every decision change is attributed to a specific user with a timestamp
- Transparency: Participants can see why the winning option changed after they voted
- Conflict prevention: Written motivations reduce disputes about organizer decisions
- Audit compliance: Corporate events often require decision documentation for compliance
Entity-Linked Polls: Venue Voting
The TipoEntitaSondaggio enum defines five categories that determine what
a poll is about. While CUSTOM polls are free-form, the specialized types like LUOGO,
DATA, MENU, and ATTIVITA carry semantic meaning that enables richer functionality.
The LUOGO (Venue) type is the most feature-rich. Options in a venue poll can be linked
to actual venue entities in the system through entitaRiferimentoId, and each
option can carry additional metadata such as a description and an image URL.
ENTITY TYPES: TipoEntitaSondaggio
├── LUOGO → Venue selection (supports winner, rich options)
├── DATA → Date selection
├── MENU → Menu/catering preferences
├── ATTIVITA → Activity selection
└── CUSTOM → Free-form, no entity linkage
VENUE OPTION (OpzioneSondaggio with entity link):
├── testo: "Seaside Conference Center"
├── entitaRiferimentoId: 156 (links to Luogo entity)
├── descrizione: "Modern facility with ocean views, 200-person capacity"
├── immagineUrl: "/images/venues/seaside-center.jpg"
├── ordine: 0
└── risposte: Set<RispostaSondaggio> (votes for this option)
FACTORY METHODS:
├── aggiungiOpzione("Simple text option") → basic option
└── aggiungiOpzioneLuogo("Venue name", luogoId, desc, imgUrl)
→ entity-linked option with metadata
This design enables the poll UI to display rich venue cards instead of plain text options. Participants can see photos, read descriptions, and make informed decisions rather than voting on abstract names.
Configurable Behaviors
Beyond the response type and state management, each poll carries a set of behavioral
flags that fine-tune the voting experience. These are configured through the
configura() method while the poll is still in BOZZA state.
CONFIGURABLE FLAGS
1. anonimo (Anonymous)
├── Default: false
├── true → Voter identity hidden in results
└── false → Voter identity visible to organizers
2. permettiModificaVoto (Allow Vote Modification)
├── Default: true
├── true → Participants can change their vote while poll is active
└── false → Vote is final once cast
3. mostraRisultatiPrimaVoto (Show Results Before Voting)
├── Default: false
├── true → Results visible to all, even before voting
└── false → Results visible only after voting or when poll is closed
4. maxOpzioniSelezionabili (Max Selectable Options)
├── Default: 1 (SINGOLA) or 3 (MULTIPLA)
├── Minimum: 1 (enforced with IllegalArgumentException)
└── Controls checkbox limit in MULTIPLA mode
5. dataScadenza (Expiry Date)
├── Default: null (no expiry)
├── When set: Poll auto-expires when date passes
└── isVotabile() checks both state AND expiry
RESULT VISIBILITY LOGIC:
sonoRisultatiVisibili(haVotato):
├── stato == CHIUSO? → true (always visible)
├── mostraRisultatiPrimaVoto? → true (config override)
└── haVotato? → true (reward for participation)
└── otherwise → false
The Survey System: Structured Feedback
While polls are designed for voting among predefined options, the Survey (Questionario) system in Play The Event addresses the complementary need for open-ended feedback. Surveys allow organizers to ask questions that require text answers, collect structured data through typed question fields, and gather participant opinions in a format that goes beyond binary choices.
Like polls, the Questionario entity is an Aggregate Root that owns its
questions (DomandaQuestionario), each of which can have predefined options
(OpzioneDomanda). Completed surveys are tracked through
CompilazioneQuestionario objects containing individual
RispostaQuestionario answers.
SURVEY (Questionario) - Aggregate Root
├── id: Long
├── evento: Evento (optional, null for templates)
├── tipo: TipoQuestionario (TEMPLATE | EVENTO)
├── templateOrigineId: Long (tracks origin template)
├── titolo: String (required)
├── descrizione: String (optional)
├── etichetta: String (max 50 chars)
├── coloreEtichetta: String (hex color, e.g. "#3B82F6")
├── stato: StatoQuestionario (BOZZA | PUBBLICATO | CHIUSO | ARCHIVIATO)
├── dataScadenza: LocalDateTime (optional)
├── anonimo: Boolean (default: false)
├── richiediEmail: Boolean (default: false)
├── richiediNome: Boolean (default: false)
├── pubblico: Boolean (default: false)
├── codiceCondivisione: String (UUID, unique)
├── creatoDa: User
├── domande: List<DomandaQuestionario> (ordered by ordine ASC)
├── creatoIl: Instant
└── aggiornatoIl: Instant
Survey State Machine
The survey lifecycle is slightly different from polls, introducing an additional ARCHIVIATO (Archived) state for long-term storage after analysis is complete.
STATE MACHINE: StatoQuestionario
pubblica() chiudi() archivia()
BOZZA ──────────► PUBBLICATO ──────────► CHIUSO ──────────► ARCHIVIATO
(Draft) (Published) (Closed) (Archived)
TRANSITION RULES:
├── BOZZA → PUBBLICATO: requires at least 1 question
├── PUBBLICATO → CHIUSO: only published surveys can be closed
├── CHIUSO → ARCHIVIATO: only closed surveys can be archived
└── BOZZA is the only modifiable state (questions, config)
KEY DIFFERENCE FROM POLLS:
├── No reopening (CHIUSO is final for compilations)
├── Additional ARCHIVIATO state for lifecycle completion
└── No cancellation state
Question Types
Each question in a survey is typed through the TipoDomanda enum. The type
determines both the UI rendering and the validation rules for responses.
APERTA (Open Text)
Free-text questions where participants provide their own written response. No predefined
options are allowed. This is ideal for subjective feedback, suggestions, or detailed
explanations. The response is stored as testoRisposta in the
RispostaQuestionario entity.
SCELTA_SINGOLA (Single Choice)
Participants select exactly one option from a predefined list. The question must have
at least one OpzioneDomanda attached. Responses store the selected option
ID as a serialized JSON array in opzioniSelezionateIds.
SCELTA_MULTIPLA (Multiple Choice)
Participants can select multiple options from the predefined list. Like single choice, it requires predefined options, and selected IDs are stored as a JSON array.
QUESTION TYPES: TipoDomanda
APERTA (Open Text)
├── richiedeOpzioni(): false
├── Options: not allowed (cleared if type changes)
├── Response: testoRisposta (TEXT column)
├── Example: "What would you improve about this event?"
└── UI: textarea input
SCELTA_SINGOLA (Single Choice)
├── richiedeOpzioni(): true
├── Options: required (OpzioneDomanda list)
├── Response: opzioniSelezionateIds (JSON array with 1 ID)
├── Example: "How would you rate the venue?"
│ ├── ○ Excellent
│ ├── ○ Good
│ ├── ○ Average
│ └── ○ Poor
└── UI: radio buttons
SCELTA_MULTIPLA (Multiple Choice)
├── richiedeOpzioni(): true
├── Options: required (OpzioneDomanda list)
├── Response: opzioniSelezionateIds (JSON array with N IDs)
├── Example: "Which sessions did you attend?"
│ ├── ☑ Opening keynote
│ ├── ☐ Workshop A
│ ├── ☑ Panel discussion
│ └── ☑ Closing ceremony
└── UI: checkboxes
TYPE CHANGE BEHAVIOR:
When aggiornaTipo() changes type to one that does not require options,
all existing options are automatically cleared (opzioni.clear())
Survey Compilations
When a participant completes a survey, their entire set of answers is bundled into a
CompilazioneQuestionario (Compilation) entity. This serves as the
aggregate container for all individual responses, linking them to the participant and
the survey instance.
COMPILATION (CompilazioneQuestionario)
├── id: Long
├── questionario: Questionario
├── utente: User (null for public compilations)
├── nomeCompilatore: String (auto-filled from User or provided)
├── emailCompilatore: String (for public compilations)
├── fingerprint: String (for duplicate prevention)
├── risposte: List<RispostaQuestionario>
└── compilatoIl: Instant
TWO CREATION MODES:
1. Authenticated User:
CompilazioneQuestionario.creaPerUtente(questionario, utente)
→ utente is set, nomeCompilatore auto-filled from User entity
→ Requires questionario.isCompilabile() == true
2. Public (Anonymous):
CompilazioneQuestionario.creaPubblica(questionario, nome, email, fingerprint)
→ utente is null, nome/email provided by respondent
→ fingerprint tracks browser for duplicate prevention
→ Requires questionario.isPubblicoCompilabile() == true
INDIVIDUAL ANSWER (RispostaQuestionario):
├── compilazione: CompilazioneQuestionario (parent)
├── domanda: DomandaQuestionario (which question)
├── testoRisposta: String (for APERTA questions)
├── opzioniSelezionateIds: String (JSON "[1,3,5]" for choice questions)
└── creatoIl: Instant
Survey Templates and Deep Copy
Just like polls, surveys support a full template system. A template survey contains questions and their options, and when instantiated for an event, the system performs a deep copy of the entire question tree, including nested options.
TEMPLATE DEEP COPY
Questionario.creaDaTemplate(template, evento, user)
│
├── Copy survey-level fields:
│ titolo, descrizione, etichetta, coloreEtichetta,
│ anonimo, richiediEmail, richiediNome
│
├── Set new fields:
│ tipo: EVENTO, stato: BOZZA, templateOrigineId: template.id
│
└── Deep copy questions:
for each DomandaQuestionario in template.domande:
│
├── DomandaQuestionario.copiaDa(originale)
│ Copy: testo, tipoDomanda, obbligatoria, ordine, descrizioneAiuto
│
└── Deep copy options:
for each OpzioneDomanda in domanda.opzioni:
│
└── OpzioneDomanda.copiaDa(originale)
Copy: testo, ordine
RESULT: Fully independent survey with new IDs,
no shared references, zero compilations
Labels and Organization
Surveys support a labeling system through EtichettaQuestionario that
helps organizers categorize and filter their surveys. Labels carry a name and a color,
and can be either system-predefined or user-created.
Label System Features
- Predefined labels: System-provided labels for common categories
- Custom labels: User-created labels with custom names and colors (hex format)
- Color coding: Each label has an associated color for visual distinction (default: #3B82F6)
- Name limit: Maximum 50 characters per label name
- User scoped: Custom labels are scoped to the user who created them
Real-Time Results
Both polls and surveys are designed to provide live result visualization. For polls, the
result visibility is governed by the sonoRisultatiVisibili() method, which
implements a tiered access model: results are always visible when the poll is closed,
optionally visible before voting if mostraRisultatiPrimaVoto is enabled,
and visible to anyone who has already voted.
Poll results aggregate vote counts per option through the
OpzioneSondaggio.getNumeroVoti() method, which returns the size of the
risposte set. For surveys, the compilation responses are aggregated by
question type: text responses can be listed or analyzed, while choice responses are
counted by selected option ID.
POLL RESULT AGGREGATION
Poll: "Where should we hold the offsite?"
Status: CHIUSO | Total votes: 24
OPTION │ VOTES │ PERCENTAGE │ BAR
──────────────────────────┼───────┼────────────┼──────────────────
Beach Resort │ 11 │ 45.8% │ ████████████
Mountain Lodge │ 8 │ 33.3% │ ████████
City Hotel │ 3 │ 12.5% │ ███
Countryside Villa │ 2 │ 8.3% │ ██
Winner selected: Beach Resort
Selected by: Alice on 2026-03-15
───────────────────────────────────────────
SURVEY RESULT AGGREGATION
Survey: "Post-Event Feedback"
Compilations: 18 responses
Q1: "How would you rate the venue?" (SCELTA_SINGOLA)
├── Excellent: 8 (44.4%)
├── Good: 7 (38.9%)
├── Average: 2 (11.1%)
└── Poor: 1 (5.6%)
Q2: "What would you improve?" (APERTA)
├── Response 1: "Better parking options"
├── Response 2: "Longer networking breaks"
├── Response 3: "More vegetarian food options"
└── ... (15 more responses)
Key Takeaways
Architectural Lessons from Polls and Surveys
- Two systems, one pattern: Polls and surveys share the same architectural patterns (Aggregate Root, state machine, templates, public sharing) while serving distinct purposes
- State machines enforce invariants: The strict state transitions prevent entire categories of bugs, such as voting on a draft poll or modifying a published survey
- Templates reduce friction: Deep copy with independent lifecycles means recurring events can reuse proven question sets without manual recreation
- Mandatory motivation creates accountability: Requiring written reasons for winner changes builds trust and creates an audit trail that participants can review
- Fingerprint-based deduplication: A pragmatic middle ground between full authentication and unrestricted access for public polls and surveys
- Domain-level validation: Business rules like minimum options, max selectable, and state guards are enforced in entity methods rather than in services or controllers
In the next article, we will explore how Play The Event handles notification delivery and real-time communication between event participants, including push notifications, in-app messaging, and email integration.







