Skip to main content

LinkedIn

A connection request isn't a boolean - it's pending, then accepted or declined, and once it's declined it should stay declined no matter how many times someone double-clicks "connect." The interesting design decision is putting that lifecycle on the request itself instead of scattering if status == "pending" checks across every place a request gets touched.

Requirements

Functional

  • A user sends a connection request to another user.
  • The recipient accepts or declines it; a connection exists only once accepted.
  • Two connected users can message each other directly.
  • Both users are notified when a request is accepted.

Non-functional

  • Invalid transitions - accepting an already-declined request, declining an already-accepted one - must be rejected at the request itself, not prevented by callers remembering to check first.
  • Notification delivery (in-app, email, push) must plug in without ConnectionRequest knowing which channels exist.

Design

ConnectionRequest carries its own state (PENDING, ACCEPTED, DECLINED) and refuses any transition that doesn't start from PENDING - the guard lives in one method, accept/ decline, not in every caller that might touch a request. That's the whole point of putting a small state machine on the object instead of a status field anyone can overwrite.

SenderConnectionRequestRecipientNotificationServicenew ConnectionRequest(sender, recipient)1accept()2state = ACCEPTED3notifyAccepted(connection)4accept()5
  1. 1A request starts life in PENDING and nowhere else.
  2. 2Only a request currently PENDING allows this call to succeed.
  3. 3The transition is guarded inside the request itself - callers never inspect state before calling.
  4. 4Notification fires only after the state change succeeds, never before.
  5. 5A second accept call on the same request throws - it is no longer PENDING.

Once a request is accepted, it notifies observers rather than calling a notification service directly - the same Observer shape as a feed fan-out, but wired to a single lifecycle event instead of every post, which is why this page's flow reads differently from Social Network's even though both lean on the same pattern.

Class diagram

«interface»RequestObserver+ onAccepted(connection)User- id: string- name: stringProfile- headline: string- experience: List<string>ConnectionRequest- sender: User- recipient: User- state: RequestState+ accept(): Connection+ decline()Connection- a: User- b: User- since: datetimeMessage- from: User- to: User- text: stringNotificationService+ onAccepted(connection)
implementsusescreates
ConnectionRequest owns its own state transitions and notifies observers only on a valid accept.

Code

import java.time.LocalDateTime;
import java.util.*;
 
class User {
final String id;
final String name;
 
User(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class Connection {
final User a;
final User b;
final LocalDateTime since = LocalDateTime.now();
 
Connection(User a, User b) {
this.a = a;
this.b = b;
}
 
boolean includes(User user) {
return a.id.equals(user.id) || b.id.equals(user.id);
}
}
 
enum RequestState { PENDING, ACCEPTED, DECLINED }
 
interface RequestObserver {
void onAccepted(Connection connection);
}
 
class NotificationService implements RequestObserver {
public void onAccepted(Connection connection) {
System.out.println(connection.a.name + " and " + connection.b.name + " are now connected");
}
}
 
class ConnectionRequest {
final User sender;
final User recipient;
private RequestState state = RequestState.PENDING;
private final List<RequestObserver> observers;
 
ConnectionRequest(User sender, User recipient, List<RequestObserver> observers) {
this.sender = sender;
this.recipient = recipient;
this.observers = observers;
}
 
Connection accept() {
if (state != RequestState.PENDING) {
throw new IllegalStateException("Only a pending request can be accepted");
}
state = RequestState.ACCEPTED;
Connection connection = new Connection(sender, recipient);
for (RequestObserver observer : observers) {
observer.onAccepted(connection);
}
return connection;
}
 
void decline() {
if (state != RequestState.PENDING) {
throw new IllegalStateException("Only a pending request can be declined");
}
state = RequestState.DECLINED;
}
}
 
class Message {
final User from;
final User to;
final String text;
 
Message(User from, User to, String text) {
this.from = from;
this.to = to;
this.text = text;
}
}
 
class LinkedInNetwork {
private final List<Connection> connections = new ArrayList<>();
private final List<RequestObserver> observers;
 
LinkedInNetwork(List<RequestObserver> observers) {
this.observers = observers;
}
 
ConnectionRequest sendRequest(User sender, User recipient) {
return new ConnectionRequest(sender, recipient, observers);
}
 
Connection acceptRequest(ConnectionRequest request) {
Connection connection = request.accept();
connections.add(connection);
return connection;
}
 
boolean areConnected(User a, User b) {
return connections.stream().anyMatch(c -> c.includes(a) && c.includes(b));
}
 
Message sendMessage(User from, User to, String text) {
if (!areConnected(from, to)) {
throw new IllegalStateException("Only connected users can message each other");
}
return new Message(from, to, text);
}
}

Design decisions

  • State lives on ConnectionRequest, guarded by its own transition methods. accept() and decline() both check that the current state is PENDING before doing anything, and throw otherwise. That check exists exactly once; without it, "don't double-accept" would need to be remembered at every call site that ever touches a request.
  • A Connection is only created as the side effect of a successful accept. There's no Connection object sitting around in a half-formed state while a request is pending - it doesn't exist until accept() decides it should, which makes "are these two people connected" a simple existence check with no partial states to account for.
  • Notification is Observer, triggered by one event: a request moving to ACCEPTED. RequestObserver implementations subscribe to ConnectionRequest; NotificationService is the only one implemented here, but a second observer (analytics, an email digest) plugs into the same notifyAccepted call with no change to ConnectionRequest or User.
  • Messaging is gated by an existing Connection, not by profile visibility rules. This page keeps Message scoped to "you're connected, so you can message" - richer visibility (InMail, open profiles) is a real LinkedIn feature but a different authorization question than the state machine this page is about.
  • What's missing for a real system: mutual-connection suggestions, request rate-limiting/ spam detection, and message read-receipts are all real features that sit outside the request lifecycle modeled here - none of them change how a request moves between its three states.
0%0 of 122 pages studied