The model
Client-server splits a system into many requesters and a smaller number of shared providers. The server owns authoritative state and enforces rules; clients render, capture input, and hold a cached, possibly stale view of that state. Communication is request-response, usually initiated by the client, over a network the client does not control.
It is the substrate of nearly everything: browsers and web servers, mobile apps and APIs, database drivers and database engines, SSH, email. Microservices are client-server between machines; a REST API is client-server with HTTP semantics.
Thin, thick and everything between
| Model | Client holds | Wins | Costs |
|---|---|---|---|
| Thin (server-rendered) | Nothing but the DOM | Instant first paint, one source of truth, trivial updates | A round trip per interaction |
| Hybrid (SSR + hydration) | Cache + some logic | Fast first paint and rich interaction | Two implementations of some logic |
| Thick (SPA / native) | Full view state, local cache, some rules | Responsive UI, works on flaky networks | Cache invalidation, duplicated rules, version skew |
| Offline-first | A full local replica | Works with no network at all | Sync engine and conflict resolution |
The trend is not linear. Teams move thick for responsiveness, then discover that shipping business rules to clients they cannot upgrade — old mobile app versions live for years — is a liability, and move authoritative rules back to the server.
The trust boundary
Which means:
- Client validation is UX; server validation is correctness. Always both, never only the first.
- Prices, discounts, entitlements and permissions are server truths. A client may display them; it may never submit them.
- Authorisation happens per request, on the server. Hiding a button is not access control.
- Assume every old client version is still live. API changes must be additive; removal requires a deprecation window measured in months.
State: who owns what
CLIENT owns view state (which tab, scroll position, form drafts) ephemeral cache (last fetched lists, with TTL + revalidation) optimistic edits (pending mutations, with rollback) SERVER owns identity & sessions money & inventory entitlements & limits audit trail anything two users can see or race on CONTESTED (decide explicitly) sort/filter preferences, unread counts, drafts, read receipts
The contested list is where bugs live. Decide per item whether the server is authoritative and the client mirrors it, or the client is authoritative and the server merely stores a blob. Both work; ambiguity does not.
Offline-first and conflict resolution
Once the client can write while disconnected, you have a distributed database with a terrible network. Options, cheapest first:
- Last-write-wins with server timestamps. Simple, silently loses data. Fine for preferences, wrong for documents.
- Version checks (optimistic concurrency). Client sends the version it read; server rejects on mismatch and the client re-fetches or prompts. Correct and explainable.
- Operation log / commands. Send intent ("add 1 to quantity"), not state ("quantity = 3"). Commutative operations merge without conflict.
- CRDTs or OT. Real concurrent editing. Powerful, and a genuine engineering project.
Whatever you choose, every offline mutation needs an idempotency key so retries after a timeout do not double-charge or double-post.
Protocol choices
- HTTP request-response for the default: cacheable, debuggable, works everywhere.
- Server-sent events when the server must push and the client only listens — notifications, progress, live counters.
- WebSockets for genuine bidirectional, low-latency traffic — chat, collaboration, trading. Costs you sticky connections and reconnection logic.
- Polling is not shameful. With ETags and a sensible interval it beats a poorly operated socket fleet.
Failure modes
| Failure | Consequence | Mitigation |
|---|---|---|
| Server unavailable | App is dead | Cached reads, queued writes, honest degraded UI |
| Timeout after the write succeeded | Duplicate orders on retry | Idempotency keys |
| Chatty client | N+1 round trips, awful on mobile | Aggregate endpoints or GraphQL/BFF |
| Version skew | Old clients hit removed fields | Additive changes, versioned contracts |
| Trusting client input | Price manipulation, IDOR | Server-side authz and recomputation |
| Thundering herd on reconnect | Server melts after an outage | Jittered backoff |
Interview framing
Strong answer
"Thick client for responsiveness, but the server stays authoritative for pricing, entitlements and inventory — the client submits intent, never computed money. Reads are cached with revalidation; writes are optimistic with idempotency keys and rollback on rejection. For live updates I'd use SSE since the flow is server-to-client only, which avoids operating a WebSocket fleet. The main risk is version skew from old mobile builds, so the API only changes additively."
Follow-ups
- What happens if the response is lost after the server committed?
- How do two devices editing the same record converge?
- How do you keep a two-year-old app version working?