Skip to main content
The Chat Server API uses a bidirectional relationship model to manage connections between users. Each relationship tracks a requester, requestee, and status.

Relationship entity

Relationships are stored as JPA entities with a unique constraint ensuring no duplicate relationships between users:
Relationship.java
@Entity(name = "relationships")
@Table(
    name = "relationships",
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"requester", "requestee"})
    }
)
public class Relationship {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "requester")
    private User requester;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "requestee")
    private User requestee;
    
    @Enumerated(EnumType.STRING)
    private RelationshipStatus status;
    
    public Relationship(User requester, User requestee) {
        this.requester = requester;
        this.requestee = requestee;
        this.status = RelationshipStatus.PENDING;
    }
}

Key features

UUID generation

Each relationship gets a unique UUID identifier

Lazy loading

User entities are loaded on-demand to improve performance

Unique constraint

Prevents duplicate relationships between the same pair of users

Enum status

Status stored as string enum in database

RelationshipStatus enum

Relationships can be in one of two states:
RelationshipStatus.java
public enum RelationshipStatus {
    PENDING,
    ACCEPTED
}
Initial state when a friend request is sent.
  • The requester has sent a friend request
  • The requestee has not yet accepted
  • Can transition to ACCEPTED when requestee accepts
  • Can be deleted if either user cancels/rejects
There is no explicit “REJECTED” status. When a user rejects a request, the relationship entity is deleted entirely.

Bidirectional relationships

Each user maintains two collections of relationships:
User.java
@Entity(name = "users")
public class User {
    // Outgoing relationships (requests I sent)
    @OneToMany(mappedBy = "requester", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Relationship> sentRequests = new HashSet<>();
    
    // Incoming relationships (requests I received)
    @OneToMany(mappedBy = "requestee", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Relationship> receivedRequests = new HashSet<>();
}

Relationship directions

When User A sends a friend request to User B:
  • Relationship is added to User A’s sentRequests
  • User A is the requester
  • User B is the requestee
  • Status is PENDING
// User A initiates friendship
relationship.getRequester().getId() == userA.getId() // true
relationship.getRequestee().getId() == userB.getId() // true
relationship.getStatus() == RelationshipStatus.PENDING // true
When User B receives a friend request from User A:
  • Relationship is added to User B’s receivedRequests
  • User B is the requestee
  • User A is the requester
  • User B can accept to change status to ACCEPTED
// User B accepts friendship
existingRelationship.setStatus(RelationshipStatus.ACCEPTED);
relationshipRepository.save(existingRelationship);

Friend request flow

The RelationshipService handles the complete lifecycle of friend requests:

Creating a relationship

RelationshipService.java
public SafeRelationship createRelationship(String userId, String otherUsername) {
    final User otherUser = userRepository
        .findByUsername(otherUsername)
        .stream()
        .findFirst()
        .orElseThrow(() -> new InvalidFriendRequestException(otherUsername));
    
    // Prevent self-friending
    if (userId.equals(otherUser.getId())) throw new SelfFriendException();
    
    Relationship existingRelationship = relationshipRepository
        .findRelationshipByUserAndOtherUser(userId, otherUser.getId())
        .orElse(null);
    
    if (existingRelationship != null) {
        // Already friends
        if (existingRelationship.getStatus() == RelationshipStatus.ACCEPTED) {
            throw new ExistingRelationshipException(...);
        }
        
        // Outgoing request already exists
        boolean outgoingRequest = existingRelationship.isOutgoingRequest(userId);
        if (outgoingRequest) {
            throw new OutgoingRequestAlreadyExistsException(...);
        }
        
        // Accept incoming request
        existingRelationship.setStatus(RelationshipStatus.ACCEPTED);
        relationshipRepository.save(existingRelationship);
        
        return new SafeRelationship(existingRelationship);
    }
    
    // Create new pending request
    final User requester = userRepository.findUserById(userId)
        .stream()
        .findFirst()
        .orElseThrow(() -> new InvalidFriendRequestException(userId));
    
    final Relationship newReq = new Relationship(requester, otherUser);
    relationshipRepository.save(newReq);
    
    return new SafeRelationship(newReq);
}

State transitions

When User B sends a friend request to User A (who already has a pending outgoing request to User B), the relationship automatically transitions to ACCEPTED instead of creating a duplicate.

Identifying request direction

The isOutgoingRequest method determines if a user is the requester:
Relationship.java
public boolean isOutgoingRequest(String id) {
    return requester.getId().equals(id) && status == RelationshipStatus.PENDING;
}
This is used to differentiate between:
  • Outgoing requests - Friend requests you sent that are pending
  • Incoming requests - Friend requests you received that are pending
  • Friends - Accepted relationships (direction doesn’t matter)

Deleting relationships

Both users can delete a relationship at any time, regardless of status:
RelationshipService.java
@Transactional
public void deleteRelationship(String userId, String otherUsername) {
    final List<Relationship> existingUserRelationships = 
        relationshipRepository.findAllRelationships(userId);
    
    for (Relationship relationship : existingUserRelationships) {
        if (relationship.getRequestee().getUsername().equals(otherUsername) || 
            relationship.getRequester().getUsername().equals(otherUsername)) {
            relationshipRepository.deleteById(relationship.getId());
            return;
        }
    }
    
    throw new RelationshipDoesNotExistException(otherUsername);
}

Deletion scenarios

Requester deletes a PENDING relationship they initiated.
DELETE /relationships/{username}
The requestee will no longer see the incoming friend request.
Deleting a relationship is permanent and cannot be undone. A new friend request must be sent to re-establish the connection.

Cascade behavior

Relationships use CascadeType.ALL on the User entity:
@OneToMany(mappedBy = "requester", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Relationship> sentRequests = new HashSet<>();
This means:
  • When a user is deleted, all their relationships are automatically deleted
  • Both sent and received requests are removed
  • The other user’s relationship list is updated automatically

Retrieving relationships

Get all relationships for the authenticated user:
RelationshipService.java
@Transactional
public List<SafeRelationship> getRelationships(String userId) {
    return relationshipRepository.findAllSafeRelationships(userId);
}
The response includes both:
  • Outgoing requests (where user is requester)
  • Incoming requests (where user is requestee)
  • Accepted friendships (both directions)

Build docs developers (and LLMs) love