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
ConnectionRequestknowing 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.
Click PENDING above and there are exactly two legal moves out of it - which is the entire
guard accept()/decline() enforce. Click ACCEPTED or DECLINED and there is nothing:
neither method does anything once the state has already moved, which is what "refuses any
transition that doesn't start from PENDING" looks like as a diagram instead of a sentence.
- 1A request starts life in PENDING and nowhere else.
- 2Only a request currently PENDING allows this call to succeed.
- 3The transition is guarded inside the request itself - callers never inspect state before calling.
- 4Notification fires only after the state change succeeds, never before.
- 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
Code
Design decisions
- State lives on
ConnectionRequest, guarded by its own transition methods.accept()anddecline()both check that the current state isPENDINGbefore 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
Connectionis only created as the side effect of a successful accept. There's noConnectionobject sitting around in a half-formed state while a request is pending - it doesn't exist untilaccept()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.RequestObserverimplementations subscribe toConnectionRequest;NotificationServiceis the only one implemented here, but a second observer (analytics, an email digest) plugs into the samenotifyAcceptedcall with no change toConnectionRequestorUser. - Messaging is gated by an existing
Connection, not by profile visibility rules. This page keepsMessagescoped 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.
Common follow-ups
- What happens if the same request is accepted twice at nearly the same instant, from two
threads? Both threads could pass the
state != PENDINGcheck before either commits its write, causing a race. A hardenedaccept()needs the state check and the transition to be one atomic operation (a lock per request, or a compare-and-swap), not a read followed by a write. - How would you let a sender cancel a request before the recipient responds? Add a
cancel()method that mirrorsaccept()/decline()'s own guard - check the state isPENDING, transition to a newCANCELLEDstate. The state machine already has the shape for a fourth branch; it just doesn't have one yet. - How do you stop a user from sending duplicate requests to the same person?
LinkedInNetwork.sendRequestwould need to check for an existingPENDINGorACCEPTEDrequest between the same two users before creating a newConnectionRequest- not currently modeled, since this page's scope is the request's own lifecycle, not lookup indexing across requests. - Where would "requests I've sent" and "requests I've received" lists live? Not as
fields mutated by
ConnectionRequestitself - more cleanly,LinkedInNetworkwould index requests by sender and recipient the same wayTagIndexindexes Stack Overflow questions: a separate lookup structure updated on creation, not state owned by the request.
Check yourself
Why does ConnectionRequest.accept() check that state is PENDING and throw otherwise, rather than trusting callers to only call it on pending requests?