Archtin
All articles
ArchitectureSystem Design10 min read

Client-Server Architecture: Drawing the Trust Boundary

Every web and mobile app is client-server. The architecture work is not the diagram — it is deciding what the client may decide alone, what only the server may confirm, and how state stays coherent across an unreliable network.

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

ModelClient holdsWinsCosts
Thin (server-rendered)Nothing but the DOMInstant first paint, one source of truth, trivial updatesA round trip per interaction
Hybrid (SSR + hydration)Cache + some logicFast first paint and rich interactionTwo implementations of some logic
Thick (SPA / native)Full view state, local cache, some rulesResponsive UI, works on flaky networksCache invalidation, duplicated rules, version skew
Offline-firstA full local replicaWorks with no network at allSync 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

The one rule
The client is an untrusted, out-of-date, user-controlled cache. Anything it computes is a hint. Anything that matters is recomputed server-side.

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
A workable split for a typical product.

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:

  1. Last-write-wins with server timestamps. Simple, silently loses data. Fine for preferences, wrong for documents.
  2. Version checks (optimistic concurrency). Client sends the version it read; server rejects on mismatch and the client re-fetches or prompts. Correct and explainable.
  3. Operation log / commands. Send intent ("add 1 to quantity"), not state ("quantity = 3"). Commutative operations merge without conflict.
  4. 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

FailureConsequenceMitigation
Server unavailableApp is deadCached reads, queued writes, honest degraded UI
Timeout after the write succeededDuplicate orders on retryIdempotency keys
Chatty clientN+1 round trips, awful on mobileAggregate endpoints or GraphQL/BFF
Version skewOld clients hit removed fieldsAdditive changes, versioned contracts
Trusting client inputPrice manipulation, IDORServer-side authz and recomputation
Thundering herd on reconnectServer melts after an outageJittered 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?

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free