Agile and Real-Time Collaboration in Event Management
Organizing an event is inherently a collaborative endeavor. Tasks must be distributed, progress must be tracked, and every participant needs to stay synchronized in real time. Traditional project management tools were not designed with the unique dynamics of event planning in mind: tight deadlines, shifting responsibilities, and the need for instant communication among organizers who may never be in the same room.
In Play The Event, we built an integrated agile workflow engine that combines sprint management, task tracking, real-time WebSocket communication, and domain-driven event architecture into a single cohesive system. This article explores every layer of that system, from the domain model to the WebSocket infrastructure that keeps everyone in sync.
What You Will Find in This Article
- Sprint management with lifecycle states and time-boxed iterations
- Task management with subtasks, priorities, dependencies, and blocking
- Multiple views: Kanban board, Gantt timeline, and grouped list
- Milestones for grouping tasks by event phases
- Story points and collaborative voting (Planning Poker)
- WebSocket architecture with STOMP for real-time synchronization
- Domain events and event-driven notification dispatching
- Multi-channel notification system (in-app, email, push)
- Activity tracking and audit log
- Integrated chat for in-event communication
Sprint Management
Agile sprints are time-boxed iterations that break down the event preparation into manageable phases. In Play The Event, each sprint is modeled as an Aggregate Root in the domain layer, with its own lifecycle, goal, and strict state transitions.
A sprint can be associated with either an event or a trip, but never both simultaneously. This mutual exclusivity is enforced at the domain level through factory methods that validate the reference at creation time. The sprint feature is available exclusively for Business and Association account types, reflecting the more structured planning needs of organizations.
SPRINT STATE MACHINE
PIANIFICATO (Planned)
├── Can transition to: ATTIVO (Active)
└── Can transition to: ANNULLATO (Cancelled)
ATTIVO (Active)
├── Can transition to: COMPLETATO (Completed)
└── Can transition to: ANNULLATO (Cancelled)
COMPLETATO (Completed)
└── Terminal state - no further transitions
ANNULLATO (Cancelled)
└── Terminal state - no further transitions
Each sprint carries a name, an optional description, a goal (up to 500 characters describing the sprint objective), a start date, an optional end date, and an order field for display sequencing. The system automatically detects expired sprints: any active sprint whose end date has passed without reaching a terminal state is flagged as overdue.
Sprint Domain Invariants
- A sprint must be associated with exactly one event or one trip (mutually exclusive)
- Name is mandatory and limited to 100 characters
- Start date is mandatory; end date (if present) must be after start date
- State transitions follow a strict finite state machine
- Terminal states (Completed, Cancelled) are irreversible
- Optimistic locking via @Version prevents concurrent modification conflicts
Task Management
Tasks are the atomic units of work within an event's preparation. The TaskEvento entity is the most feature-rich aggregate in the agile module, supporting assignment, prioritization, date ranges, dependencies, subtasks, sprint association, and story point estimation.
Task States and Transitions
Every task follows a four-state workflow that mirrors standard agile practices: TODO, IN_PROGRESS, REVIEW, and DONE. The system allows flexible bidirectional transitions between all states, recognizing that in event planning, a task marked as "done" might need to be reopened when requirements change at the last minute.
TASK WORKFLOW
TODO ──────────► IN_PROGRESS ──────────► REVIEW ──────────► DONE
◄────────── ◄────────── ◄──────────
(bidirectional transitions allowed)
Each state exposes query methods:
├── isCompletato() → true only for DONE
├── isInCorso() → true only for IN_PROGRESS
├── isInRevisione() → true only for REVIEW
├── isDaFare() → true only for TODO
└── isAttivo() → true for all states except DONE
Progression order (for sorting):
TODO=0, IN_PROGRESS=1, REVIEW=2, DONE=3
Priority and Urgency
Each task has an importance score from 1 to 10. The system defines two urgency thresholds: tasks with importance 8 or above are classified as high priority, and tasks whose deadline falls within the next 24 hours (while still incomplete) are flagged as urgent. These classifications drive visual indicators in the UI and influence notification priority.
Subtasks and Hierarchies
Tasks support a parent-child relationship through the parentTaskId field. A task can be set as a subtask of another task, creating a hierarchical structure that allows complex activities to be broken down into smaller, trackable units. The domain enforces that a task cannot be its own parent, preventing circular references at the single-node level.
EVENT: Annual Company Conference 2026
TASK HIERARCHY:
├── [PARENT] Organize catering (Priority: 9, Sprint: "Week 1")
│ ├── [SUBTASK] Contact 3 catering companies for quotes
│ ├── [SUBTASK] Review menu options for dietary restrictions
│ ├── [SUBTASK] Confirm final menu and headcount
│ └── [SUBTASK] Arrange tasting session
│
├── [PARENT] Set up AV equipment (Priority: 8, Sprint: "Week 2")
│ ├── [SUBTASK] Inventory existing equipment
│ ├── [SUBTASK] Order missing microphones and projectors
│ └── [SUBTASK] Schedule technical rehearsal
│
└── [PARENT] Send invitations (Priority: 7, Sprint: "Week 1")
├── [SUBTASK] Finalize guest list
├── [SUBTASK] Design digital invitation
└── [SUBTASK] Send via email and track RSVPs
Task Dependencies and Blocking
The dependency system allows one task to block another through the bloccaTaskId field. When task A blocks task B, the UI surfaces this relationship visually, helping organizers understand the critical path. Additionally, tasks can be manually blocked with a technical reason, useful when external factors (vendor delays, permit approvals) prevent progress regardless of the task's logical dependencies.
Concurrency Safety
The TaskEvento entity uses JPA's @Version field for optimistic locking. When two organizers attempt to modify the same task simultaneously, the second write will fail with an optimistic lock exception rather than silently overwriting the first change. The application layer catches this and prompts the user to refresh and retry, ensuring no work is lost.
Multiple Views: Kanban, Gantt, and Grouped List
Different team members think about work differently. A project manager wants a timeline view; a team lead prefers a Kanban board; a stakeholder wants a simple grouped list. Play The Event provides all three views over the same task data, with each view optimized for its specific use case.
Kanban Board
The Kanban view organizes tasks into four columns matching the task states: TODO, IN_PROGRESS, REVIEW, and DONE. Tasks can be dragged between columns to change their state, and within a column, they can be reordered by priority. Each card displays the task title, assignee avatar, priority indicator, due date, and story points badge.
Gantt Timeline
The Gantt view renders tasks as horizontal bars on a calendar timeline. Tasks with start and end dates appear as bars whose length represents their duration. Dependencies are shown as connecting arrows between bars, making the critical path immediately visible. Sprint boundaries are highlighted as colored regions on the timeline, giving context to which iteration each task belongs to.
Grouped List
The list view presents all tasks in a tabular format, grouped by sprint, assignee, or priority. Each row shows the full task details including state, dates, story points, and blocking status. This view supports bulk operations: selecting multiple tasks and changing their state, assignee, or sprint in a single action.
TASK VIEWS - SAME DATA, DIFFERENT PERSPECTIVES
┌─────────────────────────────────────────────────────────┐
│ TASK DATA STORE │
│ (TaskEvento entities with state, priority, dates, etc.)│
└────────────┬──────────────┬──────────────┬──────────────┘
│ │ │
┌───────▼───────┐ ┌───▼──────┐ ┌────▼─────────┐
│ KANBAN VIEW │ │ GANTT │ │ GROUPED LIST │
│ │ │ VIEW │ │ VIEW │
│ 4 columns by │ │ Timeline │ │ Table with │
│ task state │ │ with │ │ grouping by │
│ Drag & drop │ │ bars and │ │ sprint, │
│ reordering │ │ arrows │ │ assignee, or │
│ │ │ │ │ priority │
└───────────────┘ └──────────┘ └──────────────┘
Milestones
While sprints represent time-boxed iterations, milestones represent logical phases or deliverables in the event preparation. A milestone might be "Venue confirmed", "Invitations sent", or "Technical rehearsal complete". Milestones serve as checkpoints that give the entire team a shared understanding of progress toward the event date.
Tasks can be associated with milestones, creating a natural grouping that complements the sprint structure. When all tasks associated with a milestone are marked as DONE, the milestone is automatically flagged as achieved, providing a satisfying visual indicator of progress on the dashboard.
EVENT: Product Launch (March 15, 2026)
MILESTONES:
│
├── M1: "Venue Secured" ─────────── Feb 1 [ACHIEVED]
│ ├── Task: Research venues [DONE]
│ ├── Task: Negotiate contract [DONE]
│ └── Task: Sign agreement [DONE]
│
├── M2: "Marketing Ready" ──────── Feb 20 [IN PROGRESS]
│ ├── Task: Design press kit [DONE]
│ ├── Task: Social media campaign [IN_PROGRESS]
│ └── Task: Press release draft [REVIEW]
│
├── M3: "Logistics Complete" ───── Mar 5 [NOT STARTED]
│ ├── Task: AV setup plan [TODO]
│ ├── Task: Catering finalized [TODO]
│ └── Task: Seating arrangement [TODO]
│
└── M4: "Launch Day" ──────────── Mar 15 [NOT STARTED]
├── Task: Final rehearsal [TODO]
├── Task: Guest check-in setup [TODO]
└── Task: Post-event survey ready [TODO]
Story Points and Collaborative Voting
Estimating the effort required for each task is notoriously difficult when done in isolation. Play The Event implements a Planning Poker mechanism that allows team members to vote on the complexity of each task using the Fibonacci sequence: 1, 2, 3, 5, 8, 13, and 21.
The VotoStoryPoints entity enforces a unique constraint per task-user pair: each participant can cast exactly one vote per task, but can update their vote as long as the voting session is open. Votes are stored with timestamps and support optimistic locking to handle concurrent updates cleanly.
PLANNING POKER SESSION
TASK: "Set up live streaming infrastructure"
FIBONACCI VALUES: [1, 2, 3, 5, 8, 13, 21]
ROUND 1 - Votes hidden:
├── Alice: votes 5
├── Bob: votes 8
├── Charlie: votes 13
└── Diana: votes 5
REVEAL:
├── Min: 5 Max: 13 Spread: 8
├── Average: 7.75
└── Consensus: NOT REACHED (spread > 3)
DISCUSSION: Charlie explains complexity of multi-camera setup
ROUND 2 - Re-vote:
├── Alice: votes 8
├── Bob: votes 8
├── Charlie: votes 8
└── Diana: votes 5
REVEAL:
├── Min: 5 Max: 8 Spread: 3
├── Average: 7.25
└── Consensus: REACHED → Story Points set to 8
VALIDATION: Value must be in Set(1, 2, 3, 5, 8, 13, 21)
CONSTRAINT: One vote per user per task (uk_voti_sp_task_utente)
Why Fibonacci?
- The increasing gaps between numbers force estimators to commit to a level of complexity rather than splitting hairs between similar values
- Small tasks (1-3) are well understood; medium tasks (5-8) have some uncertainty; large tasks (13-21) signal that further decomposition may be needed
- The constraint is enforced at both the domain level (validation in the factory method) and the database level (check constraint on the column)
WebSocket Architecture and Real-Time Synchronization
The real-time layer in Play The Event is built on WebSocket with the STOMP (Simple Text Oriented Messaging Protocol) sub-protocol, using Spring's WebSocket message broker infrastructure. This architecture supports three communication patterns: broadcast to a topic, targeted messages to specific users, and request-reply interactions.
WEBSOCKET INFRASTRUCTURE
ENDPOINTS:
├── /ws (with SockJS fallback for older browsers)
└── /ws (native WebSocket for modern clients)
MESSAGE BROKER PREFIXES:
├── /topic → Broadcast to all subscribers (e.g., task updates)
├── /queue → Point-to-point messaging
└── /user → User-specific private channels
APPLICATION PREFIX:
└── /app → Client-to-server messages (e.g., send a modification)
EXAMPLE FLOW (Spreadsheet Collaboration):
1. Client connects to /ws endpoint
2. Client subscribes to /topic/foglio/{foglioId}
3. Client sends modification to /app/foglio/{foglioId}/modifica
4. Server processes and broadcasts to /topic/foglio/{foglioId}
5. All subscribed clients receive the update instantly
Presence Tracking
The WebSocket layer also handles presence management. When a user opens a collaborative resource (such as a spreadsheet or a task board), a presence message is broadcast to all other users viewing the same resource. This enables features like showing colored cursors for each active collaborator, displaying "Alice is editing cell B4" indicators, and maintaining a list of who is currently online in the event workspace.
REAL-TIME PRESENCE PROTOCOL
USER ENTERS WORKSPACE:
Client → /app/foglio/{id}/entra
Server → /topic/foglio/{id}/presenze
Payload: { utenteId, sessionId, tipo: "ENTRA", timestamp }
USER SELECTS A CELL:
Client → /app/foglio/{id}/selezione
Server → /topic/foglio/{id}/selezioni
Payload: { utenteId, sessionId, riga, colonna, timestamp }
USER LEAVES WORKSPACE:
Client → /app/foglio/{id}/esci
Server → /topic/foglio/{id}/presenze
Payload: { utenteId, sessionId, tipo: "ESCI", timestamp }
RESULT: Every connected client knows:
├── Who is currently viewing the resource
├── Which cell/element each user is focused on
└── When users join or leave
SockJS Fallback
Not all network environments support WebSocket connections natively. Corporate firewalls, proxy servers, and older browsers may block or degrade WebSocket traffic. The configuration registers both a native WebSocket endpoint and a SockJS-enabled endpoint at the same path. SockJS automatically negotiates the best available transport: native WebSocket if available, then XHR streaming, then XHR polling as a last resort. This ensures that real-time features work reliably across all environments.
Domain Events
The agile module follows an event-driven architecture where significant state changes in the domain produce events that are consumed by other parts of the system. This decouples the core business logic from cross-cutting concerns like notifications, activity logging, and analytics.
All domain events implement the DomainEvent interface, which requires an occurredOn() timestamp and provides a default eventType() method that returns the class name. The DomainEventPublisher component uses Spring's ApplicationEventPublisher to dispatch events, bridging the domain layer with the application layer without introducing coupling.
DOMAIN EVENT ARCHITECTURE
COMMAND HANDLER (Application Layer)
│
├── 1. Load aggregate (TaskEvento)
├── 2. Execute business logic (cambiaStato)
├── 3. Persist changes
└── 4. Publish domain event
│
▼
DOMAIN EVENT PUBLISHER
│ publish(StatoTaskCambiatoEvent)
│ → delegates to Spring ApplicationEventPublisher
│
├────────────────────────┬─────────────────────────┐
│ │ │
▼ ▼ ▼
NOTIFICATION HANDLER ACTIVITY LOGGER ANALYTICS HANDLER
(TaskNotificaEventHandler)
│
├── Find collaborators (assignee, creator, co-organizers)
├── Exclude the user who triggered the change
├── For each collaborator:
│ ├── Create in-app notification (NotificaUtente)
│ └── Send email (if email verified)
└── Persist all notifications
EVENTS IN THE SYSTEM:
├── StatoTaskCambiatoEvent → task state changed
├── CommentoTaskAggiuntoEvent → comment added to task
├── EventCreatedEvent → new event created
├── EventPublishedEvent → event published
└── ParticipantInvitedEvent → participant invited
Transactional Event Listeners
Event handlers use @TransactionalEventListener(AFTER_COMMIT) to ensure they only execute after the triggering transaction has been committed successfully. This prevents notifications from being sent for changes that are later rolled back. Combined with @Async and @Transactional(REQUIRES_NEW), each handler runs in its own thread and transaction, ensuring that a failure in notification delivery does not affect the original business operation.
Real-Time Notifications
The notification system in Play The Event is multi-channel: each significant action can trigger an in-app notification, an email, and optionally a push notification. The NotificaUtente entity stores the full notification lifecycle, from creation through read status tracking.
Notification Types
The system defines a comprehensive set of notification types, each with a default icon (mapped to Lucide icons for the frontend) and a default message template. Task-related notifications include:
NOTIFICATION TAXONOMY (Task-related subset)
COMMENTO_TASK
├── Icon: message-circle
├── Trigger: New comment added to a task
├── Recipients: All collaborators except the commenter
└── Channels: In-app + Email (if verified)
STATO_TASK_CAMBIATO
├── Icon: refresh-cw
├── Trigger: Task state changes (e.g., TODO → IN_PROGRESS)
├── Recipients: All collaborators except the user who changed it
└── Channels: In-app + Email (if verified)
TASK_ASSEGNATO
├── Icon: user-check
├── Trigger: Task assigned to a user
├── Recipients: The assigned user
└── Channels: In-app + Email (if verified)
ADDITIONAL TYPES:
├── INVITO_EVENTO → Event invitation
├── EVENTO_MODIFICATO → Event details changed
├── EVENTO_ANNULLATO → Event cancelled
├── EVENTO_IMMINENTE → Event starting soon
├── NUOVA_SPESA → New expense added
├── PROMEMORIA_PAGAMENTO → Payment reminder
└── MESSAGGIO_SISTEMA → System message
Notification Entity Structure
Each notification carries a title, a message, a type (enum), a read/unread flag with a timestamp for when it was read, an optional entity reference (the ID and type of the related entity), and an optional link for deep navigation. The metadata field stores additional JSON data for specialized rendering on the frontend.
Notification Design Principles
- Never notify the actor: The user who performed the action is always excluded from the notification recipients
- Respect preferences: Email notifications are only sent to users with verified email addresses
- Truncate gracefully: Task titles are truncated to 150 characters and messages to 500 characters to respect database constraints
- Deep linking: Each notification includes a link that navigates directly to the relevant resource
- Idempotent reads: Marking a notification as read is idempotent; calling it twice has no additional effect
Activity Tracking
Beyond notifications (which are user-facing), Play The Event maintains a comprehensive activity log that records every significant action taken by every user. The AttivitaUtente entity captures the activity type, a human-readable description, a timestamp, and optional references to the affected entity.
ACTIVITY TRACKING TAXONOMY
EVENT LIFECYCLE:
├── EVENTO_CREATO (calendar) "Event created"
├── EVENTO_MODIFICATO (edit) "Event modified"
├── EVENTO_PUBBLICATO (send) "Event published"
├── EVENTO_CONCLUSO (check-circle) "Event concluded"
├── EVENTO_CANCELLATO (x-circle) "Event cancelled"
└── EVENTO_POSTICIPATO (clock) "Event postponed"
TASK OPERATIONS:
├── TASK_CREATO (plus-square) "Task created"
├── TASK_MODIFICATO (edit) "Task modified"
├── TASK_COMPLETATO (check-square) "Task completed"
└── TASK_ELIMINATO (trash-2) "Task deleted"
PARTICIPANT MANAGEMENT:
├── PARTECIPANTE_AGGIUNTO (user-plus) "Participant added"
├── PARTECIPANTE_CONFERMATO (user-check) "Participant confirmed"
└── PARTECIPANTE_RIFIUTATO (user-x) "Participant declined"
EXPENSE TRACKING:
├── SPESA_AGGIUNTA (wallet) "Expense added"
├── SPESA_MODIFICATA (edit-3) "Expense modified"
└── SPESA_ELIMINATA (trash-2) "Expense deleted"
POLLS AND VOTING:
├── SONDAGGIO_CREATO (vote) "Poll created"
├── SONDAGGIO_PUBBLICATO (send) "Poll published"
├── VOTO_REGISTRATO (check-square) "Vote registered"
└── VINCITORE_SELEZIONATO (award) "Poll winner selected"
Each activity record stores:
├── UUID id
├── User reference (who performed the action)
├── TipoAttivita enum (what was done)
├── String description (human-readable detail)
├── LocalDateTime timestamp (when it happened)
├── Optional UUID entityId (what was affected)
├── Optional String entityType (type of affected entity)
└── Optional String metadata (JSON for additional context)
The activity log serves multiple purposes: it feeds the dashboard's recent activity feed, enables auditing for compliance, and provides data for analytics on team productivity and engagement patterns.
Factory Methods for Activity Creation
The AttivitaUtente entity provides three factory methods, each designed for a different level of detail. The simplest creates a basic activity record. The second adds an entity reference for linking the activity to the affected resource. The third adds arbitrary JSON metadata for specialized frontend rendering or analytics processing.
Activity vs. Notification
Activities and notifications serve different audiences. Activities are a factual record of what happened, visible on the dashboard and in audit logs. Notifications are targeted messages sent to specific users who need to be aware of a change. A single domain event (such as a task state change) typically produces both an activity record and multiple notifications, but they are created independently by different event handlers.
Integrated Chat
Real-time collaboration requires more than just task boards and notifications. Teams need a place to have quick discussions, share links, ask questions, and coordinate in the moment. Play The Event includes an integrated chat system scoped to each event, allowing participants to communicate without switching to an external messaging tool.
The chat leverages the same WebSocket infrastructure used for task synchronization and spreadsheet collaboration. Messages are broadcast to all participants subscribed to the event's chat topic, with the server enriching each message with the sender's display name, avatar, and timestamp before relaying it to subscribers.
INTEGRATED CHAT ARCHITECTURE
CLIENT (Sender)
│
├── Connect to /ws endpoint (authenticated)
├── Subscribe to /topic/evento/{eventoId}/chat
└── Send message to /app/evento/{eventoId}/chat/invia
│
▼
SERVER (WebSocket Controller)
│
├── Authenticate sender via Principal
├── Validate message content
├── Enrich with sender metadata:
│ ├── Display name
│ ├── Avatar URL
│ └── Server timestamp
├── Persist to database (chat history)
└── Broadcast to /topic/evento/{eventoId}/chat
│
▼
ALL SUBSCRIBED CLIENTS
├── Receive message in real time
├── Render in chat UI with sender info
└── Update unread message counter
The chat supports standard features including message history (loaded when a user opens the chat panel), unread message indicators, and the ability to reference specific tasks or expenses by linking to them directly from chat messages.
Putting It All Together
The agile and real-time features in Play The Event are not isolated modules but deeply interconnected layers that work together to create a seamless collaboration experience. A task state change triggers a domain event, which produces notifications (in-app and email), updates the activity log, and pushes the new state to all connected clients via WebSocket, all within milliseconds.
COMPLETE FLOW: Alice moves "Book DJ" from TODO to IN_PROGRESS
1. UI ACTION
Alice drags the task card from TODO to IN_PROGRESS on the Kanban board
2. API CALL
POST /api/events/{id}/tasks/{taskId}/stato
Body: { "nuovoStato": "IN_PROGRESS" }
3. COMMAND HANDLER
CambiaStatoTaskCommandHandler:
├── Load TaskEvento aggregate
├── Validate state transition (TODO → IN_PROGRESS: allowed)
├── Execute: task.cambiaStato(IN_PROGRESS)
├── Persist to database
└── Publish: StatoTaskCambiatoEvent
4. DOMAIN EVENT DISPATCHED
Spring ApplicationEventPublisher distributes the event
5. EVENT HANDLERS (async, after commit)
├── TaskNotificaEventHandler:
│ ├── Find collaborators: Bob (assignee), Charlie (co-organizer)
│ ├── Create NotificaUtente for Bob
│ ├── Create NotificaUtente for Charlie
│ ├── Send email to Bob (verified email)
│ └── Send email to Charlie (verified email)
│
├── ActivityLogger:
│ └── Create AttivitaUtente (TASK_MODIFICATO, "Alice moved Book DJ to In Progress")
│
└── WebSocketBroadcaster:
└── Send to /topic/evento/{id}/tasks → all connected clients update their view
6. RESULT (< 500ms total)
├── Alice sees the card move to IN_PROGRESS column
├── Bob and Charlie see the card move in real time (WebSocket)
├── Bob and Charlie receive in-app notifications
├── Bob and Charlie receive email notifications
└── Dashboard activity feed shows the change
Key Takeaways
- Domain-driven design: Aggregates (Sprint, TaskEvento) enforce all business rules, while domain events decouple state changes from side effects
- Real-time first: WebSocket with STOMP and SockJS fallback ensures every connected user sees changes instantly, regardless of their network environment
- Multi-view flexibility: Kanban, Gantt, and list views serve different mental models without duplicating data or logic
- Collaborative estimation: Planning Poker with Fibonacci story points brings team consensus to effort estimation
- Resilient notifications: Async, post-commit event handlers ensure notification failures never affect core business operations
- Complete audit trail: Activity tracking records every significant action for dashboard feeds, compliance, and analytics
In the next article, we will explore how Play The Event handles surveys, polls, and collaborative decision-making with real-time vote aggregation and winner selection.







