How the pieces of a system relate to each other — and why “who initiates the connection” turns out to be a design decision with enormous consequences.
Prerequisites: Networking 101 Time to read: ~14 minutes
Two programs need to exchange data. Someone has to start the conversation, and someone has to be reachable. That sounds trivial, but the choice determines:
Get this wrong and you end up trying to push server updates to clients that cannot be reached, or trusting a price calculation done in a browser.
One party (the server) waits at a known address. The other (the client) initiates.
flowchart LR
C1[Client] --> S[(Server)]
C2[Client] --> S
C3[Client] --> S
Why it dominates:
The consequences you design around:
| Thin client | Thick client | |
|---|---|---|
| Logic lives | Server | Client |
| Examples | Server-rendered pages, terminals | SPAs, mobile apps, desktop apps |
| Update speed | Instant — deploy the server | Slow — app store review, users who never update |
| Offline | Impossible | Possible |
| Server load | Higher | Lower |
| Trust | Server-enforced by default | Must re-validate everything server-side |
🚨 The mobile version-skew problem is a real design constraint people forget. Once you ship a mobile app, some users are on a version from two years ago and will never update. Your API must support them or you break real customers. This is why API versioning and backward compatibility matter far more for mobile than for web.
Client–server has one structural gap: the server can’t start a conversation. The client is behind a NAT, on a phone, with no reachable address.
But you constantly need server-initiated updates: a chat message arrives, a ride is matched, a price changes, a notification fires. So the industry invented a set of workarounds, and choosing between them is a common interview question.
| Technique | How | Latency | Server cost | Use when |
|---|---|---|---|---|
| Short polling | Client asks every N seconds | Up to N seconds | Wasteful — most requests return nothing | Simple, low-frequency updates; N is minutes |
| Long polling | Client asks; server holds the request open until there’s news | Near-instant | One held connection per client | Good fallback; simple to reason about |
| SSE (Server-Sent Events) | One long-lived HTTP stream, server→client only | Near-instant | One connection per client | Feeds, notifications, live dashboards |
| WebSocket | Full-duplex persistent connection | Near-instant | One connection per client | Chat, collaboration, gaming — anything bidirectional |
| Push notifications | Via APNs/FCM, works when the app is closed | Seconds | Offloaded to Apple/Google | Mobile, app not running |
📐 The cost that decides it: persistent connections mean stateful servers. 1 million concurrent WebSocket users at ~10 KB kernel state each is ~10 GB of connection memory, before your application data. And each user is pinned to a specific server, so you need a way to route a message to whichever server holds that user’s connection — usually a Redis pub/sub layer or a connection registry.
That routing problem is the heart of the chat system design question. → Realtime Communication
Every node is both client and server. There’s no central authority.
flowchart LR
A[Peer A] <--> B[Peer B]
B <--> C[Peer C]
A <--> C
C <--> D[Peer D]
A <--> D
What it buys you:
What it costs you:
Where it’s actually used: BitTorrent, WebRTC media (video calls connect peer-to-peer where possible), blockchain networks, IPFS, and some CDN-offload products that have browsers serve video chunks to each other.
🚨 In an interview, proposing pure P2P for a consumer product is usually wrong unless bandwidth cost is the dominant constraint (video distribution) or decentralization is an explicit requirement. But mentioning the hybrid — “peer-to-peer media with a central signalling server” — is a strong answer for video calling.
Almost nothing is purely one model:
The lesson: pick the model per data path, not per system. Control plane and data plane often want different answers.
The other axis people mean by “architecture” is how many layers the server side has:
Two-tier — client talks directly to the database. Fine for a desktop tool on a LAN; catastrophic on the internet (credentials in the client, no validation, no rate limiting).
Three-tier — the default, and the right starting answer for almost every design:
flowchart LR
P[Presentation<br/>web / mobile] --> A[Application<br/>business logic, APIs]
A --> D[(Data<br/>database, cache)]
N-tier — the application layer splits further into services, gateways, queues, and workers. That’s microservices, and you should only reach for it when the problem justifies it.
This distinction deserves its own emphasis because it decides whether horizontal scaling works at all.
Stateless server: keeps no client data between requests. Every request carries everything needed. Any instance can serve any request. → You can add or kill servers freely. A crash affects only the in-flight request.
Stateful server: holds session data, a WebSocket connection, an in-progress upload, or a game room in memory. → That user must reach that server. A crash loses real state. Deploys are disruptive.
The standard move: push state out of the app servers into a shared store — sessions in Redis, files in object storage, jobs in a queue. Then the app tier is stateless and trivially scalable, and the hard state problem is concentrated in systems designed for it.
Where you genuinely can’t (WebSocket connections must live somewhere), you accept stateful servers and add: a connection registry so messages can be routed, graceful drain on deploy, and client auto-reconnect logic. → Scalability
| Model | Gain | Cost |
|---|---|---|
| Client–server | Control, consistency, simplicity | You pay for all resources; single point of failure |
| Peer-to-peer | Bandwidth scales with users; censorship-resistant | Discovery, NAT traversal, trust, no consistency |
| Thin client | Instant updates, nothing to trust on the client | Higher server load, no offline, more round trips |
| Thick client | Offline, responsive, less server load | Version skew forever; must duplicate validation server-side |
| Polling | Trivial, stateless, works everywhere | Wasted requests, latency bounded by interval |
| Persistent connections | Instant delivery | Stateful servers, memory per connection, routing complexity |
Build the same tiny feature — “show a live count of connected users” — three ways:
GET /count every 2 seconds.GET /count/stream.Open 50 browser tabs against each. Watch the request count in your server logs, and the memory used by the process. The difference in server load between #1 and #3 — and the difference in complexity — is the trade-off, made concrete.