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.
- 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.