The Relationship Graph: 52 Types of Social Connections
When organizing an event, knowing who is attending is only half the picture. Understanding how people are connected to each other unlocks powerful features: intelligent seating arrangements, conflict detection, invitation suggestions, and community visualization. In Play The Event, we built a full social graph engine with 52 relationship types organized into 10 categories, weighted connections, automatic bidirectionality, and real-time D3.js visualization.
This article explores the entire relationship subsystem: from the domain model
(RelazioneUtente, TipoRelazione, StatoRelazione)
to the graph algorithms that power smart seating, friend-of-friend recommendations,
and community detection.
What You'll Find in This Article
- 52 relationship types organized into 10 semantic categories
- Weighted connections: from -1.0 (enemy) to +1.0 (parent/child)
- The
RelationshipStrengthclassification system (9 levels) - Automatic bidirectionality and inverse type determination
- Relationship state machine: PENDING, ACCEPTED, REJECTED
- Community detection and conflict identification algorithms
- Use case: graph-optimized table assignments (smart seating)
- Friend-of-friend invitation suggestions
- Interactive D3.js graph visualization
52 Relationship Types in 10 Categories
The TipoRelazione enum defines every possible connection between two
users in Play The Event.
Each type carries a weight (from -1.0 to +1.0), a display name,
and is classified into one of 10 categories. The Italian enum names are preserved
in the codebase to maintain domain language consistency.
Category 1: Core Family (weight 0.9 - 1.0)
The strongest positive relationships. These are immediate family bonds that carry the highest weight in seating and grouping algorithms.
CORE FAMILY (weight 0.9 - 1.0)
GENITORE (1.0) Parent
FIGLIO (1.0) Child (son/daughter)
CONIUGE (1.0) Spouse
PARTNER (0.95) Partner (unmarried)
GEMELLO (0.95) Twin
FRATELLO (0.9) Brother
SORELLA (0.9) Sister
Category 2: Grandparents and Grandchildren (weight 0.85)
GRANDPARENTS & GRANDCHILDREN (weight 0.85)
NONNO (0.85) Grandfather
NONNA (0.85) Grandmother
NIPOTE_DI_NONNI (0.85) Grandchild
Category 3: Uncles/Aunts and Nephews/Nieces (weight 0.7)
UNCLES/AUNTS & NEPHEWS/NIECES (weight 0.7)
ZIO (0.7) Uncle
ZIA (0.7) Aunt
NIPOTE_DI_ZII (0.7) Nephew / Niece
Category 4: Cousins (weight 0.3 - 0.6)
COUSINS (weight 0.3 - 0.6)
CUGINO_PRIMO (0.6) First Cousin
CUGINO_SECONDO (0.4) Second Cousin
CUGINO_TERZO (0.3) Third Cousin
Category 5: In-Laws (weight 0.6 - 0.75)
IN-LAWS (weight 0.6 - 0.75)
SUOCERO (0.7) Father-in-law
SUOCERA (0.7) Mother-in-law
GENERO (0.75) Son-in-law
NUORA (0.75) Daughter-in-law
COGNATO (0.6) Brother-in-law
COGNATA (0.6) Sister-in-law
Category 6: Godparents and Step-Family (weight 0.55 - 0.65)
GODPARENTS (weight 0.65)
PADRINO (0.65) Godfather
MADRINA (0.65) Godmother
FIGLIOCCIO (0.65) Godchild
STEP-FAMILY (weight 0.55 - 0.60)
PATRIGNO (0.6) Stepfather
MATRIGNA (0.6) Stepmother
FIGLIASTRO (0.55) Stepson
FIGLIASTRA (0.55) Stepdaughter
FRATELLASTRO (0.55) Stepbrother
SORELLASTRA (0.55) Stepsister
FRATELLO_CONSANGUINEO (0.7) Half-brother (blood)
SORELLA_CONSANGUINEA (0.7) Half-sister (blood)
ADOPTIVE FAMILY (weight 0.8 - 0.95)
GENITORE_ADOTTIVO (0.95) Adoptive Parent
FIGLIO_ADOTTIVO (0.95) Adopted Child
FRATELLO_ADOTTIVO (0.8) Adoptive Sibling
Category 7: Extended Family (weight 0.5 - 0.65)
EXTENDED FAMILY (weight 0.5 - 0.65)
BISNONNO (0.65) Great-grandparent
PRONIPOTE (0.65) Great-grandchild
PROZIO (0.5) Great-uncle / Great-aunt
Category 8: Social Relationships (weight 0.25 - 0.85)
SOCIAL RELATIONSHIPS (weight 0.25 - 0.85)
MIGLIORE_AMICO (0.85) Best Friend
AMICO_STRETTO (0.8) Close Friend
AMICO (0.5) Friend
COMPAGNO_CLASSE (0.4) Classmate
CONOSCENTE (0.3) Acquaintance
COLLEGA (0.3) Colleague
VICINO (0.25) Neighbor
Category 9: Organizational Relationships (weight 0.3 - 0.7)
These types cover both corporate and association contexts, enabling Play The Event to serve company events, charity galas, and association meetings equally well.
CORPORATE (weight 0.3 - 0.6)
DIPENDENTE (0.4) Employee
RESPONSABILE (0.5) Manager
DIRIGENTE (0.6) Executive / Director
COLLABORATORE (0.35) Collaborator
CONSULENTE (0.35) Consultant
STAGISTA (0.3) Intern
ASSOCIATIONS (weight 0.35 - 0.7)
ASSOCIATO (0.4) Associate / Member
MEMBRO_DIRETTIVO (0.55) Board Member
PRESIDENTE (0.7) President
VICEPRESIDENTE (0.65) Vice President
SEGRETARIO (0.6) Secretary
TESORIERE (0.6) Treasurer
CONSIGLIERE (0.5) Counselor / Advisor
VOLONTARIO (0.35) Volunteer
Category 10: Negative and Neutral (weight -1.0 to 0.0)
NEGATIVE RELATIONSHIPS (weight -1.0 to -0.2)
EX_CONIUGE (-0.3) Ex-Spouse
EX_PARTNER (-0.2) Ex-Partner
RIVALE (-0.5) Rival
NEMICO (-1.0) Enemy
NEUTRAL (weight 0.0)
SCONOSCIUTO (0.0) Unknown / Stranger
Why Negative Weights Matter
Negative relationships are not just metadata. They drive real features: the conflict detection algorithm flags when enemies or rivals are seated at the same table, and the smart seating engine actively separates people with negative connections. An ex-couple at the same wedding table is exactly the kind of disaster this system prevents.
Weight and Relationship Strength
Every relationship type carries a numeric weight between
-1.0 and +1.0. This weight is not just a label: it is
used in graph algorithms for community detection, seating optimization, and
invitation ranking. The RelationshipStrength enum maps continuous
weights into 9 discrete levels.
RelationshipStrength Classification
| Strength | Weight Range | Example Types |
|---|---|---|
| VERY_STRONG | 0.8 to 1.0 | Parent, Spouse, Best Friend |
| STRONG | 0.6 to 0.79 | Uncle, In-laws, Godfather |
| MODERATE | 0.4 to 0.59 | Friend, First Cousin, Manager |
| WEAK | 0.2 to 0.39 | Acquaintance, Colleague, Intern |
| VERY_WEAK | 0.01 to 0.19 | (custom overrides only) |
| NEUTRAL | 0.0 | Unknown / Stranger |
| SLIGHTLY_NEGATIVE | -0.3 to -0.01 | Ex-Partner, Ex-Spouse |
| NEGATIVE | -0.6 to -0.31 | Rival |
| VERY_NEGATIVE | -1.0 to -0.61 | Enemy |
The getStrength() method on TipoRelazione maps each
type to its strength level. This classification is used throughout the
application: the D3.js graph renders thicker edges for stronger connections,
the seating algorithm gives higher priority to VERY_STRONG bonds, and the
conflict detector triggers warnings only for NEGATIVE and VERY_NEGATIVE links.
public RelationshipStrength getStrength() {
if (weight >= 0.8) return VERY_STRONG;
if (weight >= 0.6) return STRONG;
if (weight >= 0.4) return MODERATE;
if (weight >= 0.2) return WEAK;
if (weight > 0) return VERY_WEAK;
if (weight == 0) return NEUTRAL;
if (weight >= -0.3) return SLIGHTLY_NEGATIVE;
if (weight >= -0.6) return NEGATIVE;
return VERY_NEGATIVE;
}
// Weight can also be manually overridden:
public void regolaPeso(double nuovoPeso) {
if (nuovoPeso < -1.0 || nuovoPeso > 1.0) {
throw new IllegalArgumentException(
"Weight must be between -1.0 and 1.0"
);
}
this.peso = nuovoPeso;
}
Automatic Bidirectionality
One of the most important design decisions in the relationship graph is automatic bidirectionality detection. When a user declares a relationship, the system automatically determines whether it is symmetric (both parties share the same type) or directional (each side has a different type).
Symmetric Relationships
Symmetric relationships are the same in both directions. If Alice is the
CONIUGE (spouse) of Bob, then Bob is also the CONIUGE
of Alice. The system stores a single record and marks it as bidirectional.
SYMMETRIC (bidirectional = true, inverse = same type)
CONIUGE, PARTNER, GEMELLO
FRATELLO, SORELLA, FRATELLASTRO, SORELLASTRA
FRATELLO_CONSANGUINEO, SORELLA_CONSANGUINEA
FRATELLO_ADOTTIVO
CUGINO_PRIMO, CUGINO_SECONDO, CUGINO_TERZO
COGNATO, COGNATA
AMICO, AMICO_STRETTO, MIGLIORE_AMICO
CONOSCENTE, COLLEGA, VICINO, COMPAGNO_CLASSE
RIVALE, NEMICO, SCONOSCIUTO
Directional Relationships
Directional relationships have a distinct inverse type. The system automatically computes the inverse so that both parties see the correct label.
DIRECTIONAL (bidirectional = false, inverse auto-computed)
GENITORE <--> FIGLIO
NONNO / NONNA <--> NIPOTE_DI_NONNI
ZIO / ZIA <--> NIPOTE_DI_ZII
SUOCERO/A <--> GENERO / NUORA
PADRINO/MADRINA <--> FIGLIOCCIO
PATRIGNO <--> FIGLIASTRO
MATRIGNA <--> FIGLIASTRA
GENITORE_ADOTTIVO <--> FIGLIO_ADOTTIVO
BISNONNO <--> PRONIPOTE
// Implementation in RelazioneUtente:
private TipoRelazione determinaTipoInverso(TipoRelazione tipo) {
return switch (tipo) {
case GENITORE -> FIGLIO;
case FIGLIO -> GENITORE;
case NONNO, NONNA -> NIPOTE_DI_NONNI;
case PADRINO, MADRINA -> FIGLIOCCIO;
case GENITORE_ADOTTIVO -> FIGLIO_ADOTTIVO;
case FIGLIO_ADOTTIVO -> GENITORE_ADOTTIVO;
case BISNONNO -> PRONIPOTE;
case PRONIPOTE -> BISNONNO;
default -> tipo; // Symmetric
};
}
Ambiguous Inverses
Some directional types have ambiguous inverses. For example,
NIPOTE_DI_NONNI (grandchild) could be the inverse of either
NONNO (grandfather) or NONNA (grandmother). In
these cases, determinaTipoInverso() returns null,
and the system requires the other party to explicitly specify the relationship
type when accepting the connection request.
Relationship State Machine
Relationships in Play The Event are not created instantly. They follow a bilateral workflow similar to social network connection requests: one user sends a request, and the other must accept or reject it.
RELATIONSHIP STATE MACHINE (StatoRelazione)
User A sends request
│
▼
┌──────────┐
│ PENDING │ Initial state (request sent)
└────┬─────┘
│
├── accetta(userBId) ──────▶ ACCEPTED (active, visible in graph)
│
└── rifiuta(userBId) ──────▶ REJECTED (inactive, preserved)
│
▼
resetRichiesta()
│
▼
PENDING (can be re-sent)
RULES:
├── Only the RECIPIENT can accept or reject
├── Only PENDING requests can be accepted/rejected
├── REJECTED requests can be reset and re-sent
├── Only ACCEPTED relationships appear in the graph
└── State tracked with timestamps for auditing
The RelazioneUtente aggregate root enforces all state transition
rules. The accetta() and rifiuta() methods validate
that the caller is the intended recipient of the request and that the current
state is PENDING. Any violation throws an exception, keeping the
domain invariants intact.
// Only the recipient can accept
public void accetta(Long utenteIdRisposta) {
validaDestinatario(utenteIdRisposta);
if (this.stato != StatoRelazione.PENDING) {
throw new IllegalStateException(
"Can only accept PENDING requests. Current: " + stato
);
}
this.stato = StatoRelazione.ACCEPTED;
this.rispostaDataIl = Instant.now();
}
// Recipient validation
private void validaDestinatario(Long utenteId) {
Long destinatarioId = getDestinatarioId();
if (!destinatarioId.equals(utenteId)) {
throw new IllegalArgumentException(
"Only the recipient (ID: " + destinatarioId +
") can respond. Provided: " + utenteId
);
}
}
Optimistic Locking
The RelazioneUtente entity uses a @Version field
for optimistic locking. If two concurrent requests try to modify the same
relationship (e.g., accept and reject at the same time), the second one will
fail with an OptimisticLockException, ensuring data consistency
without database-level locking.
Community Detection
With the social graph in place, the system can automatically identify communities: clusters of people who are densely connected to each other. This is essential for event features like automatic table grouping, activity suggestions, and understanding the social dynamics of the guest list.
COMMUNITY DETECTION IN PLAY THE EVENT
INPUT: Social graph G = (Users, Relationships)
Only ACCEPTED relationships with weight > 0
FILTER: isFamilyRelation() || isSocialRelation()
ALGORITHM: Modified Label Propagation
1. INITIALIZATION
Each user starts with a unique community label
communityOf(user) = user.id
2. ITERATION (until convergence)
For each user U:
neighbors = getAcceptedRelationships(U)
weightedVotes = {}
for each neighbor N in neighbors:
label = communityOf(N)
weightedVotes[label] += relationship.weight
communityOf(U) = argmax(weightedVotes)
3. MERGE SMALL COMMUNITIES
Communities with < 3 members are merged
with the closest neighboring community
OUTPUT: Map<CommunityId, Set<UserId>>
EXAMPLE (Wedding with 120 guests):
├── Community 1: Bride's family (28 people)
├── Community 2: Groom's family (25 people)
├── Community 3: University friends (15 people)
├── Community 4: Work colleagues bride (12 people)
├── Community 5: Work colleagues groom (10 people)
├── Community 6: Childhood friends (8 people)
└── Unassigned: loose connections (22 people)
The algorithm leverages the relationship weights: a family cluster (weights 0.7-1.0) will form a much tighter community than a group of acquaintances (weight 0.3). The result is used by the seating optimizer to ensure that each table has a cohesive social group rather than random assignments.
Conflict Detection
Negative relationships are not just passive metadata. The system actively scans the
guest list for potential conflicts using the isNegative() method on
TipoRelazione.
CONFLICT DETECTION
SCAN: For all pairs (A, B) in event participants
WHERE: relationship(A, B).weight < 0
SEVERITY LEVELS:
├── CRITICAL (weight < -0.6)
│ Types: NEMICO
│ Action: Flag to organizer, prevent same table
│
├── HIGH (weight -0.3 to -0.6)
│ Types: RIVALE
│ Action: Warn organizer, suggest separation
│
└── MODERATE (weight -0.01 to -0.3)
Types: EX_CONIUGE, EX_PARTNER
Action: Notify organizer, recommend distance
OUTPUT: List<ConflictReport>
├── userA, userB
├── relationshipType
├── severity (CRITICAL | HIGH | MODERATE)
├── recommendation (text)
└── resolved (boolean, set by organizer)
Transitive Conflicts
The system also detects transitive conflicts: if A is enemies with B, and B is best friends with C, placing A next to C could create social tension even though A and C have no direct negative relationship. The algorithm assigns a reduced conflict score to such indirect connections based on the path weight product.
Use Case: Smart Seating
One of the most powerful features enabled by the relationship graph is smart seating: automatically assigning guests to tables in a way that maximizes social cohesion and minimizes conflicts. This is a real optimization problem that organizers of weddings and large events spend hours solving manually.
SMART SEATING OPTIMIZATION
INPUT:
├── participants: List<UserId> (confirmed guests)
├── tables: List<Table> (capacity per table)
├── relationships: Graph<UserId, Relationship>
├── communities: Map<CommunityId, Set<UserId>>
└── constraints: organizer-defined rules
OBJECTIVE: Maximize total table cohesion score
score(table) = SUM(weight(A, B)) for all pairs in table
totalScore = SUM(score(table)) for all tables
HARD CONSTRAINTS:
├── No NEMICO pairs at the same table
├── Table capacity not exceeded
├── Organizer-pinned assignments respected
└── Accessibility requirements met
SOFT CONSTRAINTS (weighted penalties):
├── Keep families together (community bonus +2.0)
├── Separate EX_CONIUGE / EX_PARTNER (penalty -3.0)
├── Mix communities slightly (diversity bonus +0.5)
└── Balance table sizes (variance penalty -0.2)
ALGORITHM: Greedy + Local Search
1. Seed each table with a community core
2. Assign remaining guests by max affinity
3. Local search: swap pairs to improve score
4. Check hard constraints after each swap
5. Repeat until no improvement found
EXAMPLE OUTPUT (Wedding, 120 guests, 12 tables):
Table 1: Bride's parents + close family (score: 18.4)
Table 2: Groom's parents + close family (score: 17.8)
Table 3: Bride's extended family (score: 12.1)
Table 4: Groom's extended family (score: 11.5)
Table 5: University friends (mixed) (score: 9.2)
...
Table 12: Loose connections + colleagues (score: 4.3)
Conflicts avoided: 3 (2 ex-couples, 1 rival pair)
Why This Matters
Manual seating for a 120-person wedding typically takes 3-5 hours of negotiation between the couple and families. The smart seating algorithm produces an optimized first draft in under 2 seconds, which the organizer can then fine-tune with drag-and-drop adjustments. Each manual change is validated against the conflict rules in real time.
Invitation Suggestions
The relationship graph also powers friend-of-friend recommendations. When building a guest list, the system suggests people that the organizer might have forgotten, based on their connections to already-invited guests.
INVITATION SUGGESTIONS (Friend-of-Friend)
INPUT:
├── invitedGuests: Set<UserId>
├── organizerId: UserId
└── eventType: EventType (WEDDING, PARTY, CORPORATE, ...)
ALGORITHM:
1. For each invited guest G:
connections = getAcceptedRelationships(G)
for each connection C not in invitedGuests:
score(C) += relationship(G, C).weight
2. For each candidate C:
mutualConnections = count of invited guests connected to C
avgWeight = average weight of those connections
score(C) = mutualConnections * avgWeight
3. BOOST by relationship to organizer:
if organizer knows C directly:
score(C) *= 1.5
4. FILTER by event type:
WEDDING → boost family (isFamilyRelation * 1.3)
PARTY → boost friends (isSocialRelation * 1.2)
CORPORATE → boost colleagues (isOrganizationalRelation * 1.4)
5. SORT by score DESC, return top 20
EXAMPLE (Wedding, 80 already invited):
Suggested:
├── Mario R. (score: 4.2) - cousin of bride, friend of 3 guests
├── Laura B. (score: 3.8) - colleague + friend of 5 guests
├── Andrea M. (score: 3.1) - university friend of 4 guests
└── ... (17 more suggestions)
This feature transforms the guest list creation from a memory exercise into a data-driven process. The organizer still makes all final decisions, but the system ensures no important connection is accidentally overlooked.
D3.js Visualization
All of this data comes alive through an interactive D3.js force-directed graph rendered in the frontend. Users can explore their social network, see communities highlighted with colors, and interact with the graph in real time.
D3.JS FORCE-DIRECTED GRAPH
NODES (circles):
├── Size: proportional to connection count
├── Color: by community (auto-detected)
├── Border: gold for organizer, white for others
├── Label: user name (shown on hover)
└── Icon: relationship category icon
EDGES (lines):
├── Thickness: proportional to |weight|
├── Color:
│ ├── Green gradient → weight > 0.6 (strong positive)
│ ├── Blue gradient → weight 0.2 - 0.6 (moderate)
│ ├── Gray → weight 0.0 - 0.2 (weak)
│ └── Red gradient → weight < 0 (negative)
├── Style:
│ ├── Solid → ACCEPTED
│ └── Dashed → PENDING
└── Arrow: shown for directional relationships
FORCES:
├── charge: -300 (repulsion between nodes)
├── link distance: inversely proportional to weight
│ strongBond (w>0.8) → distance 50px
│ weakBond (w<0.3) → distance 200px
├── center: pulls graph toward viewport center
└── collision: prevents node overlap (radius + 10px)
INTERACTIONS:
├── Drag: reposition nodes (fixes position)
├── Hover: highlight node + direct connections
├── Click: show relationship details panel
├── Zoom: mousewheel zoom + pan
├── Filter: toggle by category (family, social, work)
└── Search: find and focus on a specific person
The visualization is particularly useful during event planning. The organizer can see at a glance which social clusters exist, where the bridges between groups are, and which participants are isolated (few or no connections). This information directly feeds into table assignments and activity group creation.
Performance Optimization
For events with 200+ participants, rendering all relationships at once would create visual clutter. The graph implements progressive disclosure: initially showing only STRONG and VERY_STRONG connections, with a slider control that lets the user expand to weaker relationships. The D3 simulation runs on a Web Worker to avoid blocking the main UI thread.
The RelazioneUtente Aggregate Root
The entire relationship subsystem is anchored by the RelazioneUtente
entity, which serves as the aggregate root. It encapsulates all the business logic
discussed in this article: type validation, weight management, bidirectionality,
state transitions, and recipient enforcement.
RELAZIONE_UTENTE (Aggregate Root)
FIELDS:
├── id: Long (auto-generated)
├── utenteId1: Long (first user)
├── utenteId2: Long (second user)
├── tipoRelazione: TipoRelazione (enum, 52 types)
├── peso: Double (-1.0 to 1.0, auto-set from type)
├── bidirezionale: Boolean (auto-determined)
├── tipoRelazioneInversa: TipoRelazione (auto-computed)
├── stato: StatoRelazione (PENDING / ACCEPTED / REJECTED)
├── richiestaInviataDaUtenteId: Long (requester)
├── richiestaInviataIl: Instant (request timestamp)
├── rispostaDataIl: Instant (response timestamp)
├── note: String (max 500 chars)
├── creatoIl: Instant (audit)
├── aggiornatoIl: Instant (audit)
└── versione: Long (optimistic locking)
DATABASE INDEXES:
├── idx_relazione_utente1 (utenteId1)
├── idx_relazione_utente2 (utenteId2)
├── idx_relazione_tipo (tipoRelazione)
└── idx_relazione_peso (peso)
UNIQUE CONSTRAINT:
└── uk_coppia_utenti (utenteId1, utenteId2)
FACTORY METHOD:
RelazioneUtente.crea(userId1, userId2, TipoRelazione.AMICO)
→ auto-sets: peso=0.5, bidirezionale=true,
tipoRelazioneInversa=AMICO, stato=PENDING
INVARIANTS:
├── Users cannot have a relationship with themselves
├── Duplicate pairs are prevented by unique constraint
├── Weight must be between -1.0 and 1.0
├── Only the recipient can accept/reject
└── Only PENDING requests can change state
In the Next Article
The relationship graph is one of the most distinctive features of Play The Event, turning a simple guest list into a rich social network that powers intelligent automation. In the next article, we will explore another cross-cutting concern: how the platform handles multilingual support across 7 languages, including entity translations, UI localization, and locale-aware formatting.
Key Takeaways
- 52 relationship types in 10 categories cover family, social, organizational, and negative connections
- Weights from -1.0 to +1.0 drive graph algorithms for seating, suggestions, and conflict detection
- RelationshipStrength maps continuous weights into 9 discrete levels used across the UI
- Automatic bidirectionality: symmetric types (spouse, friend) vs. directional types (parent/child)
- State machine (PENDING, ACCEPTED, REJECTED) ensures bilateral consent for all connections
- Community detection identifies social clusters for intelligent table grouping
- Conflict detection prevents ex-couples and rivals from being seated together
- Smart seating algorithm optimizes table assignments using graph weights and community data
- D3.js force-directed graph provides interactive visualization with filtering and progressive disclosure







