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:
@ 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:
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
Final state when both users are friends.
Both users can see each other as friends
Can message each other (when messaging is implemented)
Can only transition back by deleting the relationship
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:
@ 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
Outgoing requests (requester)
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
Incoming requests (requestee)
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
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:
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:
@ 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
Cancel outgoing request
Reject incoming request
Unfriend
Requester deletes a PENDING relationship they initiated. DELETE /relationships/{username}
The requestee will no longer see the incoming friend request. Requestee deletes a PENDING relationship they received. DELETE /relationships/{username}
The requester’s outgoing request is removed. Either user deletes an ACCEPTED relationship. DELETE /relationships/{username}
Both users lose access to the friendship.
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:
@ 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)