Client-Server Architecture
Client-Server Architecture is the foundational distributed-computing pattern in which one program — the client — initiates requests, and another program — the server — receives, processes, and replies to those requests. The server holds the resource, computation, or state; the client is a remote consumer of it. The pattern crystallized in the early-to-mid 1980s as networked workstations replaced shared mainframe terminals (the predecessor time-sharing model documented by Ritchie & Thompson 1974), was canonically formalized in Comer’s Internetworking with TCP/IP and Tanenbaum’s Distributed Systems textbooks, and underlies virtually every networked application written since: every Hypertext Transfer Protocol (HTTP) request, every Structured Query Language (SQL) query against a database, every gRPC call, every Lightweight Directory Access Protocol (LDAP) lookup, every X Window System render request. Roy Fielding’s 2000 dissertation (Architectural Styles and the Design of Network-based Software Architectures, ch. 5) explicitly identifies “Client-Server” as the most basic of the styles he derives, and shows that nearly every modern Web architecture is a refinement or hybrid of it. The pattern’s enduring appeal is the clarity of its ownership boundary — one party owns the resource and is the source of truth — and the simplicity of its scaling story (replicate the server). It is also intentionally partial: client-server alone does not solve high availability, multi-region replication, or peer collaboration; it is a building block on top of which richer architectures (Leader-Follower Replication Architecture, Peer-to-Peer Architecture, Microservices Architecture) are composed.
1. When to Use / When Not to Use
1.1 When Client-Server Is the Right Choice
- There is a clear authoritative resource. A database holds the canonical data; a printer is the only physical printer; a Domain Name System (DNS) zone has one administrative owner. Client-server captures this asymmetry directly.
- The server holds shared state visible to many clients. Multi-user systems where one party’s writes must be visible to other parties’ reads (banking, ticketing, collaborative editing) need a coordination point; a server is the simplest such point.
- The client cannot or should not hold the data. Mobile devices have limited storage; a thin web browser cannot replicate a five-terabyte product catalog. Pulling from a server is the only viable option.
- The work is naturally request-response shaped. A user clicks a button → the server does a thing → the user sees the result. Most user interactions fit this pattern.
- A clear administrative boundary exists. The server is operated by one organization; clients belong to many. Authentication, rate limiting, abuse mitigation, and access control all naturally live on the server.
1.2 When Client-Server Is the Wrong Choice (or Insufficient)
- All participants are symmetric peers. File sharing across millions of equivalently-trusted participants (BitTorrent, Bitcoin, Inter-Planetary File System) does not have a privileged “server” — every node is both. Peer-to-Peer Architecture is the right model.
- Network availability cannot be assumed. Mobile, embedded, and offline-first applications need to keep working when the network is gone. Pure client-server fails the moment connectivity is lost; offline-first designs (often using CRDTs Basics or local-first databases) sync to a server when one is available but do not depend on it for every operation.
- Latency to the server is unacceptable. Real-time game state, autonomous-vehicle perception, or trading-floor pricing need sub-millisecond response that round-trip-to-server cannot provide. Edge or local computation, often coordinated by eventually consistent synchronization, fits better.
- Push-style or many-to-many event flow dominates. A stock ticker pushing prices to millions of subscribers, a chat broadcasting to a room, or telemetry from a fleet of devices all fit publish-subscribe (Publish Subscribe System Design) more naturally than client-server’s pull / request-response.
- The “server” itself becomes the bottleneck. Single-server client-server hits scaling ceilings quickly. The standard remedies are additive — add load balancers, read replicas, sharding, or move to Microservices Architecture — not replacements for the basic client-server contract, but augmentations of it.
2. Structure
flowchart LR subgraph Clients["Many clients"] C1[Client 1<br/>Browser / app / SQL tool] C2[Client 2] C3[Client 3] Cn[Client N] end subgraph Server["Server side"] LB[Load Balancer<br/>optional] S1[Server Instance 1] S2[Server Instance 2] S3[Server Instance 3] DB[(Authoritative State<br/>database / disk)] end C1 -->|request / TCP / TLS| LB C2 -->|request| LB C3 -->|request| LB Cn -->|request| LB LB -->|forwarded request| S1 LB --> S2 LB --> S3 S1 --> DB S2 --> DB S3 --> DB
What this diagram shows. The general client-server topology. Many clients (the cardinality is intentionally many — N is typically much larger than the number of servers) initiate connections to the server side. In a minimal deployment there is exactly one server; in real production there is a load balancer in front of multiple identically-configured server instances. The clients do not address servers individually; they address the service (api.example.com) which the load balancer maps to a particular instance per request. Behind the server instances sits the authoritative state — typically a database, a filesystem, or a back-end of some kind — which is the real resource the clients are after; the server processes are simply the gatekeepers and request-translators. Three properties define the picture: clients initiate, never the other way around; the server is passive until pinged; and the server is the authority on whatever state is being requested. The load balancer is an optimization on top of the basic two-party model, not a fundamental change to it; from the client’s perspective the system is still “I send a request, I get a response, the server has the truth.”
3. Core Principles
The Client-Server style is defined by a handful of properties; Fielding’s 2000 dissertation (§5.1.2) enumerates them precisely.
- Asymmetric Roles. The client and server play distinct, non-interchangeable roles — client requests, server responds. A peer that does both is, by definition, not a client-server endpoint at that moment; it is a peer (Peer-to-Peer Architecture).
- Initiation by Client. All communication is started by the client. The server does not contact the client out of the blue. (HTTP/2 Server Push, WebSockets, and Server-Sent Events are partial relaxations of this rule, discussed in §5.)
- Server Owns the Resource. Whatever the protocol traffics in — rows in a database, bytes of a file, pixels of a window — is owned by the server. The client holds at most a cache or view of it.
- Separation of Concerns. Clients handle user interaction, presentation, and local input; servers handle storage, computation, and policy. This separation is what makes the client substitutable: replacing a desktop client with a mobile client without changing the server is the whole point of the style.
- Service Discovery / Addressing. The client must know how to reach the server (its address, port, protocol). In modern systems this is wrapped in a domain name resolved via DNS, possibly combined with a service-discovery layer (Service Mesh System Design for internal services).
- Statelessness Per Request (in many but not all instantiations). Pure client-server makes no requirement about session state, but Fielding’s 2000 dissertation derives the Stateless sub-style by adding the constraint that each request from a client to a server contain everything needed to understand it, with no stored context on the server. HTTP-as-deployed almost always relaxes this with cookies and sessions; the underlying protocol is stateless even when applications layered on it are not.
The deepest invariant: the server is the source of truth, and every client must reconcile through it. This is what gives client-server its appealing simplicity and what causes the patterns that compose with it (Sharded Architecture, Leader-Follower Replication Architecture, Cell-Based Architecture) to ultimately exist — they are all about scaling or replicating the server side of a client-server contract while preserving the contract.
4. Request Flow
sequenceDiagram participant C as Client (Browser) participant DNS as DNS Resolver participant LB as Load Balancer participant S as Server Instance participant DB as Database C->>DNS: resolve("api.example.com") DNS-->>C: A 203.0.113.17 C->>LB: TCP SYN to 203.0.113.17:443 LB-->>C: TCP SYN-ACK C->>LB: TLS ClientHello → handshake completes C->>LB: HTTP GET /users/42 LB->>S: forwarded GET /users/42 S->>S: parse, authenticate, authorize S->>DB: SELECT * FROM users WHERE id = 42 DB-->>S: row S->>S: serialize response (JSON) S-->>LB: 200 OK {user} LB-->>C: 200 OK {user} Note over C: Connection often kept alive for next request
What this diagram shows. A canonical client-server interaction over HTTPS, which is the most common modern instantiation of the pattern. The client initiates everything: it first asks the Domain Name System to resolve the human-readable name to an Internet Protocol (IP) address, then opens a Transmission Control Protocol (TCP) connection, then performs a Transport Layer Security (TLS) handshake, then sends an HTTP request. The load balancer makes a forwarding decision (round-robin, least-connections, consistent-hashing — see Consistent Hashing) and the chosen server instance does the actual work: parse, authenticate, authorize, query the database, serialize, respond. The response retraces the path back to the client. The server never initiates; it answers. The TCP connection is typically kept alive (HTTP keep-alive, HTTP/2 multiplexing) so the next request from the same client reuses it, amortizing the handshake cost. The diagram intentionally shows the layers (DNS, TCP, TLS, HTTP) because in interviews it is common to be asked “what happens when you type a URL into a browser?” — the answer is essentially this diagram, expanded.
5. Variants
5.1 Two-Tier (Direct) Client-Server
The minimal form: one client process talks directly to one server process. SQL clients talking to a database server are this — psql opens a TCP connection to postgres and exchanges queries. So is the original X Window System (X11): the X client (an application like xterm) talks to the X server (which owns the display hardware), and the asymmetry is opposite to what most people first guess (the “server” is the side with the screen). Two-tier deployments are still extremely common for internal tools, command-line utilities, and embedded scenarios.
5.2 Three-Tier and N-Tier Client-Server
The natural extension: add a middle tier between the client and the data store. The client talks to an application server (the “middle tier”); the application server talks to the database server. This is what most modern Web applications look like (browser → web/app server → database) and is the bridge to Layered Architecture (N-Tier). Each tier is, in isolation, still client-server (the application server is a server to the browser and a client to the database), but the overall topology now has multiple hops. The motivation was originally physical — applying business logic on a dedicated machine reduced load on expensive database servers and let many clients share one connection pool — and is now mostly logical (clean separation of concerns).
5.3 Thin Client vs Thick Client
A thin client does minimal computation; the server does almost everything. A web page rendered server-side and shipped as fully-formed Hypertext Markup Language (HTML) is a thin-client interaction; the browser’s job is just to render. A thick client (also called a fat client or rich client) does substantial work locally — caching, validation, presentation logic, even some business rules. Single Page Applications (SPAs) like Gmail or Notion are thick clients; the browser runs a substantial JavaScript application that periodically synchronizes with the server. The distinction is a continuum, not a binary. Modern web applications often combine both: server-side rendering for the first page (fast initial load) plus a thick client for subsequent interactions (responsive updates).
The thin/thick tradeoff axes deserve explicit enumeration:
- Deployment. Thin: deploy server, clients update automatically next page load. Thick: must distribute new client (app store update, installer, browser cache invalidation) and clients lag behind the server’s version.
- Offline support. Thin: requires network for every interaction. Thick: can function offline (with eventual sync), which is essential for mobile and desktop applications used in unreliable network conditions.
- Bandwidth. Thin: every interaction is a network round-trip. Thick: data fetched once, manipulated locally; bandwidth amortized over many operations.
- Initial load time. Thin: small initial download (just the HTML shell). Thick: large initial download (megabytes of JavaScript or a native installer).
- Responsiveness. Thin: every click waits on the server. Thick: most interactions are local and instant.
- Server capacity. Thin: server does all work; capacity scales with user count. Thick: clients do most work; server only synchronizes; same hardware serves more users.
- Security. Thin: server enforces all business rules; client cannot bypass. Thick: client should not be trusted to enforce security rules; sensitive validation must duplicate on the server.
- Development complexity. Thin: one codebase (server). Thick: two codebases (client and server) with sync logic that must keep them coherent.
- Multiple clients. Thin: same server serves any browser uniformly. Thick: each client platform (web, iOS, Android, desktop) is potentially a separate codebase.
A worked example contrast: Adobe Photoshop vs Figma. Adobe Photoshop is the canonical thick desktop client. The Photoshop application is gigabytes of native code running locally; it loads image files from local disk; it performs all image manipulation locally on the user’s CPU and GPU; it persists results locally. The “server” interaction is minimal — license validation against Adobe’s license server, occasional preset and brush downloads from Adobe’s content servers, telemetry. The thick-client architecture is appropriate because image manipulation is computationally intensive, latency-sensitive (the user expects every brush stroke to render instantly), and locally-meaningful (the user’s files live on their computer). Network connectivity is irrelevant to the core functionality; Photoshop works fine offline.
Figma is the canonical thin-client-with-thick-aspects design tool. The Figma client is a substantial WebGL-based JavaScript application running in the browser, but the canonical state of every design file lives on Figma’s servers. When you open a file, the client connects via WebSocket to Figma’s servers and receives the design’s current state plus a stream of subsequent edits from collaborators; every edit you make is sent to Figma’s servers and broadcast to all collaborators. The architecture is client-server (the server holds the truth) but the client is thick (substantial WebGL rendering, local caching, optimistic-update logic). The thick aspect is what makes Figma feel responsive; the client-server backbone is what makes real-time collaboration possible.
The architectural trade-offs play out concretely. Photoshop’s offline-first thick-client design means: collaborators cannot work on the same file simultaneously without an external sync layer (Adobe Creative Cloud sync, which exists but is bolted-on); the user cannot access their work from a different device without manual sync; the application must be installed before use. Figma’s online-first client-server design means: real-time collaboration is the default; the user can access their work from any device with a browser; the application requires no installation; but the application requires network connectivity for any operation, and Figma’s server is a critical-path dependency. Different tradeoffs, suited to different problem domains.
The deeper architectural lesson: the thin-thick choice is not purely technical; it reflects assumptions about how the work is done. Photoshop assumes a single user editing alone; Figma assumes a team editing collaboratively. The architecture follows from the workflow, not the other way around. Modern collaborative tools (Google Docs, Notion, Linear, Slack) all chose the thin-client-with-thick-aspects model because their value proposition is collaboration; modern desktop applications that retain the thick-client model do so because their value proposition is single-user power and offline operation.
5.4 Stateless vs Stateful Server
A stateless server keeps no per-client memory between requests; each request carries everything needed (Fielding’s Client-Stateless-Server style). HTTP-as-defined is stateless. Practical web applications layer cookies, JSON Web Tokens (JWTs), or server-side session stores on top to simulate state. A stateful server holds session state in memory between requests; this is simpler initially but defeats horizontal scaling because requests from the same client must hit the same server (sticky sessions). The shift toward stateless servers in the 2000s–2010s was driven by cloud-scaling needs.
The protocols differ sharply in their statefulness. HTTP is stateless at the protocol level — each request is independent, the server need not remember the last request from the same client. SSH is deeply stateful — once authenticated, the connection holds a shell session, environment variables, current directory, open file handles, all the way through to disconnect. SQL database connections are stateful per session — once authenticated, the connection accumulates per-session state (current transaction, prepared statements, session variables, temporary tables); closing the connection loses all of it. WebSocket is stateful — the connection itself is the session, with both ends accumulating per-connection state. gRPC is per-request stateless (each RPC is independent) but the underlying HTTP/2 connection is long-lived for performance.
The choice between stateful and stateless server profoundly affects scaling. A stateful server has affinity — once a client’s connection lands on a particular server instance, all subsequent traffic from that client must reach the same instance until the connection ends. Load balancers must implement session affinity (sticky sessions) via cookies, IP hash, or explicit session-routing logic. The result: load is unbalanced (some servers may be over-loaded with active sessions while others sit idle); failover is hard (when an instance fails, all its sessions are lost — clients must reconnect and re-establish); and scaling is per-machine rather than fleet-wide (adding capacity means provisioning more instances, but the existing affinity prevents existing sessions from migrating). A stateless server avoids all this — any instance can handle any request from any client; load balancing is round-robin or random; failover is trivial; capacity scales fleet-wide. The cost: every request must carry its session context (typically as a cookie or token), and the per-request work is slightly more (one extra cache lookup or token decoding step).
The practical compromise that dominates modern Web architecture: stateless application servers backed by a stateful session store. The application server is itself stateless — any request can land anywhere — but per-client session data lives in a shared cache (Redis, Memcached) or token (JWT signed by the server) that any application server can access. This combines the operational simplicity of statelessness with the developer convenience of having a session abstraction. Microsoft’s IIS state service, ASP.NET Core’s distributed cache, Rails’s session-store gem, and Express’s connect-redis all implement this pattern.
5.5 Synchronous vs Asynchronous Client-Server
Synchronous: the client blocks waiting for the response (the standard request-response shape). Asynchronous: the client submits a request and continues; the server processes it later, and the client polls or receives a callback when ready (long-polling, WebHooks, callback URLs, or queues). The asynchronous variants are the bridge to Event-Driven Architecture and Message Queue System Design.
5.6 Push-Augmented Client-Server
Server-Sent Events (SSE), WebSockets, and HTTP/2 Server Push partially relax the “client always initiates” rule by letting the server push data over a connection the client opened. The connection still originated client-side (so the directional asymmetry is preserved at the transport level), but the application-level direction can become bidirectional. Real-time chat, collaborative editing, live dashboards, and stock tickers use this. Strictly, this is a hybrid: client-server at session-establishment time, peer-ish during the session.
The push-augmented variants matter for system design because they are the answer to “how do you tell the client about updates that originated outside the client’s request?” The pure client-server answer is the client must poll — which is wasteful when updates are infrequent (a typical chat window polled every 5 seconds will most often poll into emptiness, wasting both bandwidth and server cycles) and slow when updates are urgent (a 5-second poll interval means up to 5-second latency before the client sees an update that already exists on the server). The push-augmented variants close this gap.
Specific tradeoffs across the push mechanisms:
- Long polling. The client opens an HTTP request that the server holds open until it has data to send (or until a timeout). The simplest push mechanism — works through any HTTP-compatible network. Cost: each “long” request still eventually closes and is replaced by a new request, so the wire activity is similar to frequent polling. Latency is good for the first update; subsequent updates may suffer from re-establishment latency.
- Server-Sent Events (SSE). A single HTTP connection that the server keeps open and writes events to incrementally. Browser support is built-in (the EventSource API). Auto-reconnect on network failure. One-way (server to client). Used by GitHub for repository event feeds, by various financial-data services for live price tickers, by progress-update UIs.
- WebSockets. A bidirectional persistent connection upgraded from an initial HTTP request. Both ends can send at any time. Used for chat, multiplayer games, collaborative editing, live dashboards. Cost: each WebSocket connection consumes server resources persistently, which is expensive at scale unless the server runtime is connection-optimized (Node.js with cluster mode, Go, Erlang/Elixir, Rust + Tokio).
- HTTP/2 Server Push. Allows the server to proactively send resources the client will likely need next (e.g., when the client requests
index.html, the server preemptively pushesstyle.css). The mechanism has been deprecated in modern browsers (Chrome removed support in 2022) due to complexity and limited benefit; modern best practice uses HTTP/2 prioritization and 103 Early Hints instead.
The choice among these is workload-driven. SSE for one-way push to many clients (notifications, news feeds, log streams). WebSockets for bidirectional real-time (chat, games, collaborative editing). Long polling as a fallback when the network refuses to allow persistent connections (some corporate firewalls). HTTP/3 Server Push is largely abandoned. The architectural decision is not “which is best” but “which fits this workload’s update-frequency, bandwidth, and connection-lifetime profile.”
5.7 RPC-Style Client-Server
Birrell and Nelson’s 1984 paper Implementing Remote Procedure Calls introduced Remote Procedure Call (RPC), in which the client invokes a function and the server executes it — making the network call look like a local function call. This is still client-server (asymmetric, client-initiated), just with a function-invocation veneer over the request-response wire format. gRPC, Apache Thrift, JSON-RPC, and historical XML-RPC, Sun RPC, and Distributed Component Object Model (DCOM) are all instances. The contrast is with Representational State Transfer (REST), which keeps the resource-and-verb shape of HTTP rather than hiding it behind function-call syntax.
5.8 REST as a Client-Server Refinement
Fielding’s 2000 dissertation derives REST by adding constraints to the basic Client-Server style: stateless, cacheable, uniform interface, layered system, optional code-on-demand. REST is not a different architecture from client-server; it is a constrained client-server, with constraints chosen to make Web-scale deployment work.
5.9 Modern Wire Protocols Beyond HTTP
The 2010s and 2020s have seen a proliferation of wire protocols layered on top of basic client-server semantics, each tuned for a specific category of workload. Understanding the full set is the difference between a candidate who says “I’d use REST” for everything and one who can articulate why a specific protocol fits a specific problem.
gRPC (2015 open-sourced from Google’s internal Stubby). A binary-protocol RPC framework using Protocol Buffers for message serialization and HTTP/2 for transport. Designed for service-to-service communication inside a data center, where the participants control both ends and can deploy in lockstep. Compared to REST: dramatically lower per-call overhead (binary serialization, header compression, HTTP/2 multiplexing); strict typing via Protocol Buffer schemas; supports streaming (client-streaming, server-streaming, bidirectional-streaming) as first-class call types in addition to unary request-response. Used at Google, Netflix, Cloudflare, Square, and most large tech companies for internal service-to-service communication. The cost: poor browser support (gRPC over HTTP/2 cannot be invoked directly from a browser; gRPC-Web is the workaround but adds complexity), requires Protocol Buffer schemas (a benefit for typed contracts but an adoption cost), and has a steeper learning curve than REST. Strong fit for internal microservices; weaker fit for browser-facing APIs.
GraphQL (2015 open-sourced from Facebook). A query language for APIs and a runtime for fulfilling those queries. The client specifies exactly the shape of the data it needs; the server returns that shape. The architectural inversion: the client is in control of the query, not the server (which traditionally exposed fixed endpoint shapes). The benefit: a single API can serve many clients with different needs without each needing custom endpoints. The cost: query complexity becomes a server-side concern (a malicious client can request a deeply-nested query that explodes computationally; defense via query-depth limits and query-cost analysis), and caching is harder because every client’s query may be different (the GraphQL community has built tooling for normalized client-side caching). Used by GitHub’s API, Shopify’s storefront API, and many SaaS APIs that serve diverse client applications. Strong fit when client teams iterate frequently and the API needs to flex with them.
WebSocket (RFC 6455, 2011). Persistent bidirectional connection over which both client and server can send messages at any time. The client initiates the connection (preserving client-server’s directional asymmetry at session establishment), but once established, either side can speak. Used for real-time applications: chat (Slack, Discord), collaborative editing (Google Docs, Figma), live dashboards (Grafana, Bloomberg terminals), multiplayer games. The cost: persistent connections consume server resources (one process / thread / file-descriptor per connection until released), which is expensive at scale unless using async runtimes (Node.js, Erlang/Elixir, Tokio).
Server-Sent Events (SSE, HTML5). Server-pushed events over a long-lived HTTP connection. Simpler than WebSocket (one-way: server-to-client only; uses regular HTTP/HTTPS so works through proxies and firewalls more easily; auto-reconnect built into browsers’ EventSource API). Used when the server needs to push updates and the client doesn’t need to send (notification feeds, log streams, progress updates).
HTTP/3 over QUIC (RFC 9114, 2022). The next-generation HTTP, replacing TCP with QUIC over UDP. The architectural significance for client-server: eliminates head-of-line blocking at the transport layer, dramatically improves performance over lossy networks (mobile, satellite), and reduces handshake latency from 2 round-trips (TCP + TLS) to 0 (QUIC’s session resumption). The client-server semantics are unchanged; only the transport is faster.
MQTT (ISO/IEC 20922, originally IBM 1999). A lightweight publish-subscribe protocol designed for constrained devices and unreliable networks (IoT sensors, vehicular telematics, home automation). Technically broker-mediated rather than direct client-server, but each device’s connection to the broker is client-server. Used massively in IoT deployments because of its small wire-protocol footprint (minimal CPU, bandwidth, and battery cost on devices).
WebRTC. Peer-to-peer audio/video/data, but with a signaling phase that is client-server (peers exchange connection metadata via a server before establishing the peer connection). Used by video-conferencing tools (Google Meet, Microsoft Teams, Zoom for some deployments) and increasingly for low-latency data channels in collaborative applications.
WebTransport (in development as of 2026). A successor to WebSocket built on HTTP/3 / QUIC. Provides bidirectional streams and unreliable datagram support natively (WebSocket is reliable-only and ordered-only, which is sometimes overkill for real-time scenarios). Browser support is partial as of 2026 but growing; expected to replace WebSocket for new applications over the coming years.
Apache Cassandra and DynamoDB native protocols. Many NoSQL databases ship their own wire protocols (the Cassandra Native Protocol, DynamoDB’s HTTP-based protocol). Each is technically client-server with the database server holding the data and clients issuing CRUD requests, but the wire protocols are tuned for the specific access patterns these databases support (consistent-hashing-aware request routing in Cassandra, partition-key-aware request routing in DynamoDB).
The choice among these is workload-driven. REST for simple resource-oriented APIs with caching needs; gRPC for high-throughput internal service calls with strict schemas; GraphQL for client-controlled queries against complex data; WebSocket for real-time bidirectional; SSE for one-way push; HTTP/3 transparently for performance on lossy networks; MQTT for constrained devices; WebRTC for peer audio/video with central signaling. A modern system often uses several simultaneously — REST for the public API, gRPC for internal service calls, WebSocket for real-time updates, all in the same overall architecture.
6. Real-World Examples
The list of real-world client-server deployments is essentially the list of all networked applications. Some specific cases worth knowing:
- The World Wide Web. Berners-Lee’s 1989 CERN proposal Information Management: A Proposal explicitly described a client-server architecture (browsers as clients, web servers as servers). HTTP, currently codified in RFC 9110 (2022) and previously RFC 2616 / RFC 7230, is the canonical modern client-server protocol. Roughly every page load on Earth is a client-server interaction.
- SQL Database Clients.
psql, MySQL Workbench, JDBC / ODBC drivers, language-level clients likepg(Node.js) orpsycopg(Python) all open a TCP connection to a database server and exchange queries. The protocol (Postgres wire protocol, MySQL binary protocol) is a request-response client-server protocol with extensions for prepared statements and result-set streaming. - DNS. Recursive resolvers (the “client” in the upstream direction) talk to authoritative name servers (the “server”). The request-response semantics are pure client-server, on UDP for short queries and TCP for longer ones.
- Email (SMTP, IMAP, POP). Mail clients (Outlook, Thunderbird) talk to mail servers via Simple Mail Transfer Protocol (SMTP, for sending) and Internet Message Access Protocol (IMAP, for reading) or Post Office Protocol version 3 (POP3, also for reading).
- X Window System (X11). Client applications send drawing requests to the X server, which owns the display and input devices. The directional naming (the application is the client, the screen is the server) is famously confusing but the asymmetry is canonical client-server.
- Secure Shell (SSH). SSH clients connect to SSH servers (sshd). The server holds the shell session.
- Git over HTTP/SSH.
git pushandgit pullare client-server interactions with a Git server; the on-disk repository on the server is the authoritative state. - gRPC and other RPC systems. Internal microservice calls in modern stacks are typically gRPC over HTTP/2 — client-initiated, request-response, with Protocol Buffers as the wire format. Google internally documented Stubby (gRPC’s predecessor) as the canonical RPC for service-to-service communication.
- CDN edges. Content Delivery Network nodes (Content Delivery Network System Design) are servers from the user’s browser’s perspective; from their own perspective they are clients to the origin servers. The same client-server contract recurses.
- IDE Language Servers (LSP). The Language Server Protocol (LSP, Microsoft 2016, https://microsoft.github.io/language-server-protocol/) standardizes how editors communicate with language-specific analysis tools. The editor (VS Code, Neovim, Sublime, Emacs) is the client; the language server (rust-analyzer, gopls, pyright, TypeScript Server) is the server. The protocol is JSON-RPC over stdio or TCP; the editor sends
textDocument/completionrequests when the user types., the server responds with a list of completions; the editor sendstextDocument/didChangenotifications when the user edits a file, the server updates its analysis. Before LSP, every editor had to implement language-specific intelligence per language (M editors × N languages = M×N integrations); LSP collapsed it to M+N (each editor speaks LSP once, each language ships one server). The architecture is canonical client-server with an interesting wrinkle: the “server” runs locally on the user’s machine, often in a separate process. Same conceptual asymmetry as a web server, different deployment topology.
The “client” in client-server need not be a human-driven user agent. Automated peers — cron jobs, Continuous Integration build runners, monitoring probes, scheduled batch processors — are clients in the strict architectural sense whenever they initiate request-response interactions with a server. The distinction matters because automated clients have different operational characteristics than human-driven clients: they are typically more predictable in load patterns (a cron job runs every five minutes; a human’s clicks are bursty), they require more disciplined retry-with-backoff (a misbehaving cron loop can DDoS its target if not careful), and their “user agent” is typically a service identity rather than an end user (authentication is via service-to-service credentials or signed JWTs, not user passwords). Modern microservice architectures have many more automated clients than human clients; the same client-server architectural pattern applies, but the operational profile differs.
7. Tradeoffs
| Dimension | Client-Server — pro | Client-Server — con |
|---|---|---|
| Conceptual simplicity | Crystal clear ownership of state | Forces every interaction through the server, even when peers could communicate directly |
| Centralized control | Authentication, authorization, rate-limiting, audit all live in one place | The server is a single point of attention for security, scaling, and reliability |
| Substitutable clients | Same server can serve browser, mobile app, partner API | API design becomes a public contract; breaking changes are expensive |
| Scaling story | Replicate the server, add a load balancer | Server is the bottleneck; client scaling is “free” but uninteresting |
| Reliability | Server uptime determines system availability | Single point of failure unless replicated; requires Leader-Follower Replication Architecture or equivalent for high availability |
| Latency | Predictable round-trip (one hop) for any request | Bounded below by the network round-trip; very-low-latency requirements push compute toward the edge or the client |
| Network requirement | Works across any TCP/IP network | Fails entirely when the network is unavailable; offline-first applications must layer additional discipline |
| Statefulness | Server can hold session state for convenience | Sticky sessions limit horizontal scaling; stateless is preferred at scale |
| Security model | Trust boundary at the server | The whole system trusts the server; if the server is compromised, all clients are compromised |
| Caching | HTTP-style cacheable responses dramatically reduce server load | Cache invalidation is hard; stale-data bugs are common |
| Push-style flows | Possible via WebSockets, SSE, long-poll | Each adds complexity; pure client-server is pull-only |
| Multi-region | One server geographically far is slow for distant clients | Solving this requires Multi-Region Active-Active Architecture or CDN edges — additions on top of client-server |
The deepest tradeoff: client-server gives you a clear semantic (one source of truth) at the cost of a centralized point that must be scaled and protected. Most distributed-system architecture work is, in some sense, about preserving client-server’s semantic clarity while replacing the single-point-of-truth with a distributed group that behaves like a single server.
8. Migration Path
8.1 Into Client-Server
The most common entry: a single program (a desktop application talking to a local file) needs to be multi-user. The migration is well-trodden:
- Extract the data. The local file becomes a database accessed over the network.
- Define a request/response protocol. What operations does the client want; what shape of response satisfies them.
- Stand up a server. Implement the request handler.
- Authenticate. A multi-user system needs to know who is on the other end of each connection.
Historically this was the path from MS-DOS desktop applications to client-server LAN applications in the late 1980s and from monolithic mainframe terminals to networked workstations in the same era.
A modern echo: the same migration shape recurs whenever a single-user offline application needs multi-user features. The 2010s saw many desktop applications (Adobe Creative Cloud, JetBrains IDEs, Office 365) add cloud-sync features that turned them into client-server hybrids. The thick client kept its substantive local computation; cloud servers were added for sync, license validation, and collaborative features. Each addition followed the same pattern: extract data from local storage to the cloud server; define a sync protocol; authenticate users; handle network failure gracefully with offline-first semantics. The lesson: client-server is not a one-time migration but a continuous design pressure; whenever “this application would benefit from being multi-user” or “this application needs cloud backup,” the same migration discipline applies.
8.2 Out of (or Beyond) Client-Server
Common destinations:
- Toward Layered Architecture (N-Tier) / 3-Tier. Insert an application server between the client and the database. The original 2-tier becomes 3-tier. This is the natural growth path of every successful client-server application that gains business logic.
- Toward Microservices Architecture. The single server explodes into many cooperating services. Each pair (client → service, service → service) is still client-server at the protocol level; the architecture-level shift is that no single service is “the server” anymore.
- Toward Peer-to-Peer Architecture. When the central server’s role is mostly bandwidth-multiplexing rather than authority (file sharing, video conferencing’s media plane), peer-to-peer can replace client-server entirely.
- Toward Edge Computing Architecture. When latency to the central server is the bottleneck, push compute and a partial replica of the data to the edge (CDN POPs, Cloudflare Workers, AWS Lambda@Edge). The basic client-server contract is preserved at the edge; the edge then talks back to the origin asynchronously.
- Augment with Publish Subscribe System Design. Client-server alone is pull-only; adding a pub-sub channel (server-sent events, WebSocket-based pub-sub, or back-channel queue) lets the server proactively notify interested clients.
8.3 Migration Pitfalls
- Trying to make the server stateless after the fact. Sessions, caches, and per-client memory accrete in stateful servers; ripping them out late is expensive. Better to design stateless from the start.
- Forgetting client retries. A naïve client that does not retry on transient failure makes the system look less reliable than the server actually is. Idempotent operations and exponential backoff are part of the client responsibility (Retry with Backoff Pattern).
- Concentrating too much business logic on the client. A “thick client” that holds rules the server should enforce becomes a security and maintenance liability — a malicious client can simply omit the validation. Client-side validation is for user experience; server-side validation is for correctness.
- Underestimating client-server protocol versioning. Once a client is in users’ hands, the server must support the protocol version the client speaks for as long as that client is in use. Mobile applications particularly suffer from this — users can run two-year-old app versions; the server must support the protocol from two years ago. This is not a problem the monolith faces (the client and server are deployed together) but is a major concern for any system with externally-distributed clients. The discipline: design the wire protocol with extensibility in mind from day one, never break backward compatibility without an explicit deprecation cycle.
- Ignoring the partial-failure problem. When a request fails halfway, the client often does not know whether the server received and processed it, received and dropped it, or never received it. Each requires different recovery. The robust pattern is idempotent operations with idempotency keys — the client retries with the same key, the server deduplicates. Stripe’s API is the canonical reference; many other modern APIs (Square, Shopify, Razorpay) follow the same idiom.
9. Pitfalls and Anti-Uses
- Single Point of Failure. A single-instance client-server deployment goes down with one machine. Defense: replicate the server, fronted by a load balancer; for stateful servers, add Leader-Follower Replication Architecture or Leaderless Replication Architecture.
- Server Becomes Performance Bottleneck. The server’s CPU, memory, or database connection pool saturates before the clients do. Defense: vertical scale up to the limit, then horizontal scale (load balancer + replicas + sharding), then caching (LRU Cache, Consistent Hashing-routed cache fleet), and ultimately Microservices Architecture to split the workload.
- Sticky Sessions Defeating Horizontal Scaling. A server keeping per-client state in memory forces the load balancer to send each client to the same instance. If that instance fails, the session is lost. Defense: stateless servers; if state is needed, store it in a shared cache (Redis) or in a JWT carried by the client.
- Chatty Protocol Latency. A client that needs five sequential server round-trips to render one screen pays the network round-trip-time five times. Defense: batch, use GraphQL-style “ask once for the graph you need,” HTTP/2 multiplexing, or denormalized read views.
- Client Trusting the Server Implicitly. A malicious or compromised server can tell the client anything. Defense: TLS for transport authenticity, certificate pinning for sensitive applications, end-to-end encryption when the server should not see the content (Signal, Matrix end-to-end-encrypted modes — these intentionally limit the server’s role).
- Server Trusting the Client Implicitly. Validation only on the client lets a malicious client bypass it. Defense: every business rule enforced server-side; client-side validation as user-experience optimization only.
- No Backpressure. The server, overwhelmed, accepts more work than it can handle and falls behind monotonically. Defense: explicit rate limits (Token Bucket, Sliding Window Rate Limiter), bounded request queues, Circuit Breaker Pattern-style fast failure.
- TCP Connection Exhaustion. Many short-lived clients each opening and tearing down TCP connections waste the server’s file-descriptor budget. Defense: HTTP keep-alive, HTTP/2 multiplexing, connection pools client-side, careful tuning of the server’s maximum concurrent connections.
- Cache Inconsistency. The client (or an intermediary cache like a CDN) holds stale data after the server’s truth changes. Defense: short TTLs for volatile data, ETags / If-Match for conditional requests, explicit purge on writes, Cache Aside Pattern disciplines.
- Network-Failure Confusion. A client cannot distinguish “server received my request and crashed before responding” from “server never received my request.” The two require different recovery. Defense: idempotency keys, server-side request deduplication, idempotent operation design.
- Implicit Coupling on Server-Internal Behavior. A client that depends on undocumented response ordering, error-message phrasing, or nullable-versus-empty-string distinctions makes the server’s internal changes visible as breakage. Defense: explicit, versioned API contracts; consumer-driven contract testing.
- Polling When Push Would Suffice. Many clients pulling every few seconds for “any updates?” wastes most of the work — the answer is almost always “no.” Defense: long-poll, server-sent events, WebSockets, or a Publish Subscribe System Design back-channel.
10. Comparison With Sibling Architectures
| Property | Client-Server | Peer-to-Peer Architecture | Microservices Architecture | Master-Worker |
|---|---|---|---|---|
| Symmetry | Asymmetric (client / server) | Symmetric peers | Asymmetric pairs (each call client→service) | Asymmetric (master orchestrates workers) |
| Single source of truth | Yes — the server | No — distributed | Per service | Yes — the master |
| Scaling | Replicate server | Add peers (capacity grows naturally) | Per-service scaling | Add workers |
| Failure isolation | Server failure = service down (without replicas) | One peer’s failure tolerable | Per-service blast radius | Master failure = total stop without backup |
| Coordination cost | Low (centralized) | High (consensus, gossip) | Medium (per-pair) | Low (master decides) |
| Best for | Multi-user shared resource | Symmetric-trust file sharing, decentralized currency | Large-team multi-product organizations | Embarrassingly parallel batch (MapReduce, Spark) |
| Example | HTTP, SQL, X11, gRPC | BitTorrent, Bitcoin, IPFS | Netflix, Amazon | MapReduce, Apache Spark |
Client-server is the atom of distributed-system topology. The other architectures either (a) are compositions of many client-server pairs (microservices, sharded systems, master-worker) or (b) reject the asymmetry deliberately (Peer-to-Peer Architecture). When a system claims to be “post client-server,” it is almost always an N-way client-server arrangement that has been re-narrated; the bytes still move via request-response between named endpoints.
The deep contrast with Peer-to-Peer Architecture is worth elaborating because it sharpens what client-server is by negation. In a peer-to-peer architecture, every node is both client and server simultaneously — it serves resources to others while consuming resources from others. There is no privileged “source of truth” node; the network’s value comes from the symmetric many-to-many relationships. BitTorrent is the canonical case: every peer downloading a file is simultaneously uploading file chunks to other peers, with the whole network’s bandwidth aggregated to deliver the file faster than any single server could; there is no central server that owns the file (a tracker exists to bootstrap discovery, but the data itself flows peer-to-peer). Bitcoin and similar cryptocurrencies are another case: every node holds a copy of the ledger and validates transactions against it; no node is privileged. The InterPlanetary File System (IPFS) generalizes the model further.
The trade-offs are sharp. Peer-to-peer architectures have capacity that grows with users (more peers = more aggregate bandwidth = faster delivery), no single point of failure (the network survives any subset of peer failures), and no central operator cost (no servers to pay for). The costs: trust is distributed (every peer is a potential adversary; the network must tolerate Byzantine faults; cryptographic verification is the load-bearing mechanism); coordination is hard (achieving consensus across many independent peers is the main reason consensus protocols like Raft, Paxos, and various Byzantine fault-tolerant variants exist); and latency is unpredictable (peer availability fluctuates). Client-server architectures have clear authority (the server is the source of truth; clients trust the server within the trust boundary), predictable performance (server’s capacity is known and engineered), and centralized operability (one place to deploy, monitor, secure). The costs: single point of failure (mitigated by replicas but never fully eliminated); capacity does not grow with users (more clients require more server capacity, paid for by the operator); and trust assumes server integrity (a compromised server can manipulate all clients).
The choice is workload-driven. When the system’s value is in symmetric sharing among trust-equal participants (file distribution at scale, decentralized currency, censorship-resistant communication), peer-to-peer wins. When the system’s value is in centralized authority over some resource (banking, e-commerce, multi-user collaborative editing with a canonical version), client-server wins. Most real-world systems are client-server because most useful work involves authority over some resource; peer-to-peer architectures are powerful but niche.
A subtle hybrid worth flagging: many modern systems use peer-to-peer with central signaling. WebRTC for video conferencing is the prototype: peers exchange audio/video directly to minimize latency (peer-to-peer for the data plane), but a central server coordinates discovery and connection negotiation (client-server for the control plane). Modern collaborative editing tools (Google Docs, Linear, Figma) follow a similar pattern: peers may exchange operational-transformation operations directly via WebSocket, but a server holds the canonical state and coordinates conflict resolution. The hybrid captures the bandwidth and latency benefits of peer-to-peer for the bulk data while preserving the authoritative-state benefits of client-server for correctness.
11. Common Interview Discussion Points
- “What happens when you type a URL in a browser?” A canonical answer walks the client-server interaction layer-by-layer: DNS resolution, TCP handshake, TLS handshake, HTTP request, server processing (load balancer → application server → database), response back, browser rendering. Mention HTTP keep-alive and HTTP/2 multiplexing for follow-up.
- “Client-server vs peer-to-peer — when to choose which?” Client-server when there’s a clear authoritative resource and an asymmetric trust model. Peer-to-peer when participants are symmetric and the system’s value is in many-to-many sharing rather than centralized authority.
- “What is REST and how is it related to client-server?” REST is a constrained client-server — Fielding 2000 derives it by adding statelessness, cacheability, uniform interface, etc., on top of the basic Client-Server style.
- “Stateful vs stateless server — tradeoffs?” Stateful: simpler initial design, sticky sessions, hard to scale horizontally, lose state on restart. Stateless: harder initial design (need external session store or token-based session), trivially horizontal-scaling, restart-tolerant.
- “How would you achieve high availability for a client-server system?” Replicate the server. Front it with a load balancer. For the database, Leader-Follower Replication Architecture with read replicas and an automatic failover mechanism (synchronous replication for the leader’s writes if you want zero data loss; asynchronous for higher throughput). Cite specific implementations: Postgres streaming replication, MySQL Group Replication, Redis Sentinel.
- “Thin client vs thick client?” Thin: server does everything, client just renders. Thick: client does substantial work locally. Modern Single Page Applications are thick clients; server-rendered pages are thin. Tradeoffs: bandwidth, latency, perceived responsiveness, developer complexity.
- “How does HTTP map to the client-server model?” HTTP is the canonical modern client-server protocol. Each request is independent (stateless at the protocol level). Cookies and sessions are application-level state layered over the stateless protocol.
- “Why is push so hard in client-server?” Because the server cannot initiate. Workarounds: long-polling (client opens a long-lived request the server holds until it has data), Server-Sent Events (server streams over a one-way connection the client opened), WebSockets (full-duplex over a connection the client upgraded). All of these preserve the client-initiated property at session establishment.
- “How would you design a client-server system to scale to 1M concurrent connections?” TCP-level: tune
ulimit, use epoll/kqueue/IOCP-based async servers (Node.js, Netty, Tokio); HTTP-level: HTTP/2 to multiplex many requests per connection; topology: many server instances behind a load balancer; for stateful real-time systems, shard connections by user ID via Consistent Hashing across the server fleet. Discord is a concrete reference point: by July 2017 it served “nearly five million concurrent users” with each connection backed by one Erlang/ElixirGenServersession process on the BEAM virtual machine, with a single session-VM able to hold up to 500,000 live sessions (Discord engineering, 2017); by 2019 they had pushed past 11 million concurrent users after replacing hot Elixir data structures with Rust via Rustler-built Native Implemented Functions (NIFs) (Discord engineering, 2019). - “What’s the fallacy of distributed computing that bites client-server hardest?” Probably “the network is reliable” (Deutsch+Gosling). Every client-server design must handle the case where the response never arrives — through idempotency, retries with backoff, and circuit breakers.
12. Historical Context and Lineage
The Client-Server pattern emerged from a specific transition in computing economics. Through the 1960s and 1970s, the dominant computing model was time-sharing: a single mainframe computer with many “dumb” terminals — devices like the IBM 3270, the DEC VT100, the ASCII teletype — that did no local computation. Users at each terminal interactively typed commands; the mainframe ran every program, stored every file, and shipped character output back to the terminal. The architecture was centralized; the asymmetry between the mainframe and the terminal was so extreme that “client” and “server” were not yet useful labels.
By the late 1970s, two technological shifts converged. First, the microprocessor (Intel 8080 in 1974, Motorola 68000 in 1979) made it economical to put nontrivial compute power into the user’s terminal — turning the terminal into a workstation. Second, Ethernet (Metcalfe & Boggs 1976) and TCP/IP (Cerf & Kahn 1974, deployed in the early 1980s) made it cheap to connect those workstations into a Local Area Network. The architectural opportunity opened: split the application into a part that runs on the workstation (presentation, local computation) and a part that runs on a shared resource (database, file storage, printer). The former is the client; the latter is the server. This is the moment the modern terminology was forged, in places like Xerox PARC (1970s networked office), MIT’s Project Athena (1983, with X11 emerging from it), and Sun Microsystems’ Network File System (1984).
The mainframe-to-PC transition of the 1980s deserves more detailed treatment because it shaped both the technology and the economics of client-server computing. Through the 1970s, business computing meant IBM mainframes (System/370, later System/390) running batch processing for overnight reports and online transaction processing during the day, accessed by users via dumb terminals (the IBM 3270 was the dominant model). All compute happened centrally; the terminal displayed characters and accepted keystrokes. The economics were: one large capital expenditure for the mainframe, ongoing per-user costs for additional terminals (cheap) and software licenses (expensive). The asymmetry was so extreme that the cost of computing was concentrated at the mainframe; users barely had a financial footprint.
By the early 1980s, the IBM PC (1981) and the Apple Macintosh (1984) had put substantial compute power into individual hands at 5,000 per machine. The architectural opportunity: rather than buying more mainframe capacity (~$1M per upgrade), an organization could buy hundreds of PCs and connect them to existing mainframes or to dedicated database servers. The mainframe-replacement-by-network-of-PCs movement was branded “downsizing” in the IT trade press and was the dominant business-computing trend of the late 1980s. The architectural pattern that enabled it was client-server: PCs as clients, mainframes (or new dedicated database servers) as servers.
The dominant killer app driving this adoption was the SQL client connecting to a relational database server. The 1980s saw the launch of relational databases as commercial products (Oracle in 1979, Sybase in 1984, Microsoft SQL Server in 1989, IBM DB2 in 1983) running on dedicated server machines, with PCs running query tools (Lotus 1-2-3 with database connectivity, dBase, FoxPro) connecting via proprietary protocols (Sybase Open Client, Oracle SQL*Net, IBM Distributed Relational Database Architecture / DRDA). The client-server pattern was generalized from this: any service-providing computer with multiple consuming computers fit the same conceptual model.
A specific historical curiosity is the X Window System (X11), developed at MIT’s Project Athena starting in 1984 and officially released in 1987. X11’s choice of which side to call “server” is famously confusing because it inverts the modern intuition. In X11, the server is the program controlling the user’s display, keyboard, and mouse — the local hardware. The client is an application like xterm, Emacs, or a graphical engineering tool — possibly running on a remote machine (a beefy server in the lab) and rendering its UI to the user’s local X server. The naming follows the abstract definition: the server owns the resource (the display); the client requests services from the resource owner (asks the display to draw things). The X11 architecture was prescient: it established that “local hardware” could be a server when seen as a resource provider, and that compute work could happen anywhere on the network with the display as the integration point. This anticipated the modern thin-client model (browser as the display server, the cloud as the client) by 30 years.
The X11 naming has confused a generation of engineers because the modern shorthand “server = big remote machine, client = local small machine” inverts the canonical meaning. New engineers introduced to client-server through web programming form an intuition that gets violated when they later encounter X11. The pedantic correction is that “server” originally meant the side providing the service / owning the resource, and the modern shorthand is a coincidence of typical Web deployment, not a definition. Understanding the X11 case is what separates a candidate who can reason about the abstract pattern from one who has only learned the Web instance.
The 1980s and early 1990s were the two-tier era: client applications connecting directly to database servers via proprietary protocols (Sybase Open Client, Oracle SQL*Net, IBM DB2 client). The downside was tight coupling between client and database — a schema change broke every client application. The mid-1990s response was three-tier: insert an application server in the middle, holding business logic, with the client talking only to the application server and the application server talking to the database. This is what made the Web possible: the browser is the (thin) client; the web server plus application server are the middle tier; the database is the back tier. Berners-Lee’s 1989 CERN proposal explicitly framed the Web in client-server terms.
A specific milestone in the 1990s evolution was Sun Microsystems’ 1995 launch of Java, with the explicit positioning “Write Once, Run Anywhere” intended as a client-side technology — Java applets running in the browser as a heavier alternative to HTML. The applet model failed to dominate (security model issues, browser support fragmentation, slow startup times), but Java succeeded enormously on the server side, becoming the dominant language for the application-server tier in 3-tier architectures throughout the 2000s. The resulting J2EE / Java EE specifications (1999 onward) codified the 3-tier and 5-tier client-server architecture in framework-supported form, with application servers (BEA WebLogic, IBM WebSphere, JBoss) hosting the business-tier components and managing client connections. For a generation, “client-server architecture” in enterprise IT meant “Java application server with database server, accessed by browsers.”
The mid-2000s saw the rise of AJAX (Asynchronous JavaScript and XML, named in Jesse James Garrett’s 2005 essay) — the technique of fetching server data asynchronously from a JavaScript-running browser without a full page reload. AJAX began the slow shift from thin-client server-rendered pages to thick-client Single Page Applications. By the 2010s, frameworks like Backbone.js (2010), AngularJS (2010), Ember (2011), React (2013), and Vue (2014) had made the SPA pattern dominant for new applications, with the server reduced to a JSON-emitting REST API. The architectural shift: from server holds presentation logic to server holds data and business logic, client holds presentation logic. This is still client-server, but the asymmetry has shifted — the server is still authoritative on data, but presentation has moved to the client. This shift continues, with frameworks like Next.js (2016), Remix (2021), and Server Components (React 2023) re-introducing some server-side rendering for performance and SEO reasons. The pendulum has swung; the architecture remains client-server.
Throughout the 2000s and 2010s, the client-server model proliferated into every networked context: REST (Fielding 2000) refined client-server with explicit constraints for Web-scale deployment; gRPC (Google’s open-sourcing of Stubby in 2015) brought efficient cross-language RPC to the modern era; WebSockets (RFC 6455, 2011) added bidirectional communication while preserving the client-initiated property at session establishment.
Fielding’s 2000 dissertation (Architectural Styles and the Design of Network-based Software Architectures, UC Irvine PhD) deserves more detailed treatment because it is the most-cited theoretical work on Web architecture and explicitly derives modern client-server style from first principles. Fielding’s method was structural: start with the Null style (no constraints) and add constraints one at a time, deriving named styles as the cumulative effect of constraint sets. The derivation he presents:
- Null style — no constraints. Pure abstract distributed system.
- Client-Server (CS) — add: separation of concerns between user interface (client) and data storage (server). This produces the basic asymmetric request-response pattern we have been discussing.
- Client-Stateless-Server (CSS) — add: server keeps no per-client state between requests. Each request must contain everything needed to understand it. Enables horizontal scaling of server fleets.
- Client-Cache-Stateless-Server (C$SS) — add: responses are labeled cacheable or non-cacheable. Caches between client and server can serve cached responses without bothering the origin server. Dramatically reduces server load and latency for read-heavy workloads.
- Layered Client-Cache-Stateless-Server (LC$SS) — add: layered system constraint. The client cannot tell whether it is talking directly to the origin server or to an intermediary (proxy, load balancer, gateway, CDN); the intermediary can implement transparent transformations. This is what allows AWS to put CloudFront in front of an EC2 server without the client knowing.
- Uniform Layered Client-Cache-Stateless-Server (ULC$SS) — add: uniform interface. All resources are accessed via the same set of methods (GET, POST, PUT, DELETE, etc.); resource representations are standardized. Generality at the cost of efficiency for specialized cases.
- Code-on-Demand (COD) — optional addition: server can ship executable code (JavaScript, originally Java applets) that the client executes. This extends client functionality without redeploying the client.
The combination of constraints 1–6 plus optional 7 is what Fielding named REST (Representational State Transfer). Each constraint has a reason — Fielding’s prose argues why each is necessary for Web-scale deployment. Statelessness is necessary for horizontal scaling. Cacheability is necessary to serve read-heavy traffic without saturating the origin. Layered system is necessary for intermediary deployment (proxies, CDNs, gateways) without client awareness. Uniform interface is necessary for generic tooling (browsers, search-engine crawlers, link-checkers, etc., that work without per-resource knowledge).
Reading Fielding 2000 carefully is the difference between an engineer who uses REST and one who understands why REST is the way it is. Most “REST APIs” in the wild violate one or more of Fielding’s constraints (typically the uniform-interface or cacheability constraints, by using verbs in URIs or making every response uncacheable); Fielding has expressed displeasure with this on his blog over the years (the famous 2008 REST APIs must be hypertext-driven post). The pragmatic Web is a relaxed REST; the canonical REST is what Fielding articulated, and the constraints have engineering reasons that the relaxed version often forgets.
Critically, every more advanced distributed architecture is built on top of client-server primitives. A microservices fleet is many client-server pairs. A leader-follower replicated database is client-server with the leader as the canonical server and the followers as both servers (to read clients) and clients (to the leader). A Kafka cluster is a client-server protocol where producers and consumers are clients to the broker. Even peer-to-peer systems like BitTorrent run client-server interactions during the brief moments when one peer requests a chunk from another.
13. The Stateless Constraint and Why It Matters
Fielding’s 2000 dissertation derives a Client-Stateless-Server refinement of the basic style by adding the constraint: each request from the client must contain everything needed to understand it; the server keeps no per-client memory between requests. This sounds austere; the payoff is enormous and worth understanding precisely.
When a server is stateful — keeping per-client session memory across requests — it has affinity to specific clients. The system’s load balancer must route each client’s requests to the same server (sticky sessions); if that server fails, the client’s session is lost; horizontal scaling becomes per-machine rather than fleet-wide; the server’s memory grows with the user count. Each of these is solvable, but they are the costs of statefulness.
When a server is stateless — every request carries its own context, typically as a JSON Web Token (JWT) cookie or an explicit session ID looked up against a shared session store like Redis — none of these problems exist. Any server can handle any request from any client; the load balancer can be ignorant of identity; failover is trivial (the new server starts processing the next request without any handoff); horizontal scaling is fleet-wide. The cost is that every request carries more bytes (the cookie or token), and the per-request work is slightly more (one extra cache lookup or token decoding step).
For Web-scale systems, this trade strongly favors statelessness. It is one of the load-bearing reasons HTTP scaled to billions of users; the same Apache or Nginx server fleet can serve any user, with no preallocated state. This property is so central to modern Web architecture that engineers often forget it required design effort to achieve — early Web servers were stateful (CGI sessions in shared memory), and the move to stateless servers with externalized session storage was an architectural shift through the late 1990s and 2000s.
The corresponding stateless property does not hold for the underlying database. Databases are inherently stateful; every Web-scale stateless application server fleet sits in front of a state-bearing database that is itself replicated, sharded, or otherwise scaled by stateful means (Leader-Follower Replication Architecture, Sharded Architecture, Consistent Hashing). The architectural pattern is: stateless front, stateful back, with the boundary being the layer where session lookups happen. This is the modern incarnation of client-server.
14. Case Studies and Protocol Comparisons
14.0.4 The Web’s Original Client-Server Asymmetry
A specific moment in Web history is the 1991 release of the first web browser (NCSA Mosaic was 1993; Berners-Lee’s WorldWideWeb browser was 1990, available externally in 1991). Berners-Lee’s 1989 CERN proposal explicitly described HTTP as a client-server protocol, with the deliberate design choices of statelessness (a server forgets each client between requests, allowing horizontal scaling) and uniform interface (any browser can talk to any web server using the same set of methods). The early Web was extremely thin-client — the browser displayed HTML the server had pre-rendered, with no JavaScript and no local computation. Server power was expensive; client (PC) power was relatively scarce; the architecture matched the resource distribution.
The subsequent 30 years have seen the resource distribution invert: client power (modern CPUs, GPUs, large RAM) is plentiful while server power is the scarce resource that must be amortized across many users. The architectural response has been the gradual thickening of the browser client — adding JavaScript (1995), AJAX (2005), Single Page Applications (2010s), WebAssembly (2017+), WebGL/WebGPU for client-side graphics — to push computation back toward the user’s machine. The basic client-server contract (server holds truth, client requests services) has remained constant; only the distribution of work has shifted. This is one of the cleanest examples of how the same architectural pattern can serve dramatically different resource economies over time, with each generation’s architecture being defensible given its hardware reality.
14.0.5 Worked Example: Photoshop vs Figma — Thick-Client vs Thin-Client Architectural Trade-offs in Detail
Returning to the Photoshop-Figma contrast (introduced in §5.3) for a more detailed architectural treatment, because it is the cleanest production case of how the thin/thick decision shapes everything downstream.
Photoshop’s architecture (thick client). The application is a 1+ GB native binary running locally on Mac or Windows. It loads PSD/JPG/PNG files from local disk; image manipulation runs on the local CPU and GPU; results persist to local disk. Network operations are minimal: license validation against Adobe’s license server (perhaps once per launch), occasional synchronization with Adobe Creative Cloud Storage if the user has enabled it, telemetry. The client-server contract is narrow: the server is essentially a license-and-storage utility, not the canonical state holder. Adobe operates relatively few Photoshop servers because most computation happens on user machines.
Figma’s architecture (thin client with thick aspects). The application loads as a JavaScript bundle (~MB sized) into the browser; once loaded, it establishes a WebSocket to Figma’s servers and downloads the document’s current state plus the sequence of edits from collaborators. All edits the user makes are optimistically applied locally for responsiveness, then sent to the server which broadcasts to other collaborators. The server is the canonical state holder — Figma’s servers hold the current version of every design file, plus the operation log used to merge concurrent edits. The client is thick in the sense that it does substantial WebGL rendering and local edit application, but thin in the sense that the canonical state lives elsewhere.
The architectural consequences differ profoundly. Photoshop must manage file format compatibility across many versions (a 2026 Photoshop user might open a file created in 2008); Figma manages protocol compatibility (the WebSocket protocol must remain backward compatible across client versions). Photoshop must handle offline-first; Figma handles real-time sync. Photoshop’s failure modes are local (crash, file corruption); Figma’s failure modes are network-related (server outage, partition during edit). Photoshop’s competitive moat is feature density and computational power; Figma’s moat is collaboration and zero-installation.
The deeper lesson: the thin-thick choice is not a “which is better” question. It is a “which fits the workflow” question. Single-user computational power → thick. Multi-user collaboration → thin (or thin-thick hybrid). The architecture follows the workflow.
14.1 HTTP as the Universal Client-Server Protocol
The Hypertext Transfer Protocol, currently codified in RFC 9110 (2022) with related specifications RFC 9111 (HTTP Caching), RFC 9112 (HTTP/1.1), RFC 9113 (HTTP/2), and RFC 9114 (HTTP/3), is the most-deployed client-server protocol in human history. Every web browser is an HTTP client; every web server is an HTTP server; every REST API and most modern Remote Procedure Call frameworks (gRPC over HTTP/2, JSON-RPC over HTTP, WebDAV, GraphQL) are HTTP-layered.
HTTP’s design choices reflect deliberate client-server style refinements:
- Request methods (GET, POST, PUT, DELETE, PATCH) encode the intent of the request, separating safe operations (GET, HEAD) from unsafe operations (POST, PUT, DELETE) and idempotent operations (GET, HEAD, PUT, DELETE) from non-idempotent ones (POST, PATCH). This taxonomy guides client and intermediary behavior — caches can store GET responses safely; load balancers can retry idempotent requests on transient failure; client libraries can implement automatic retries.
- Status codes (200 OK, 301 Moved Permanently, 401 Unauthorized, 503 Service Unavailable) encode the response’s category, making client error-handling regular.
- Headers carry metadata orthogonal to the request body: authentication, content negotiation, caching directives, custom application metadata.
- Caching semantics (Cache-Control, ETag, Last-Modified, If-Match) let intermediaries (browser caches, Content Delivery Networks, reverse proxies) participate in the client-server conversation transparently. This is the property that makes the Web scale: a CDN cache hit replaces an origin-server round trip.
HTTP/2 (2015) added multiplexing — one TCP connection carrying many concurrent requests — eliminating the head-of-line blocking that HTTP/1.1 suffered. HTTP/3 (2022) replaced TCP with QUIC over UDP, eliminating head-of-line blocking also at the transport layer. Both retained the client-server semantics; only the wire-level efficiency changed.
14.2 SQL Database Clients
Postgres’ wire protocol, MySQL’s binary protocol, and SQL Server’s Tabular Data Stream (TDS) protocol are all client-server protocols where the server holds the data and the client issues queries. A SELECT statement is a request; a result set is a response. Prepared statements are a small extension: the client sends a parameterized SQL once (a Parse + Bind-style request) and then issues many Execute requests with different parameter bindings, amortizing the parsing cost.
The interesting property of database client-server protocols: they are connection-oriented and stateful per session. Once a client logs in, the connection accumulates per-session state (current transaction, prepared statements, session variables, temporary tables). Closing and reopening the connection loses all this state. This is why connection pools (pgbouncer for Postgres, ProxySQL for MySQL, JDBC connection pools for any Java application) exist — they amortize the connection-establishment cost across many requests, paying a small per-request cost (lease/return from the pool) instead of a large per-request cost (fresh handshake plus authentication).
A specific operational consideration: the number of database connections a server accepts is bounded by available memory and process resources. PostgreSQL allocates approximately 10 MB of working memory per connection (settable via work_mem and similar parameters); a 256 GB server can theoretically host thousands of connections but in practice runs better with a few hundred. Connection pooling is essential for any application with more concurrent clients than the database can directly serve. The pooler (PgBouncer, PgPool-II, RDS Proxy) presents itself as a database server to many client connections and multiplexes them onto fewer real database connections. This is a layered client-server arrangement — the application is a client to the pooler; the pooler is a client to the database; the database serves both. The client-server semantics nest cleanly.
Modern serverless databases (AWS Aurora Serverless v2, Neon, PlanetScale, Supabase) explicitly address the connection-scale problem by providing built-in connection multiplexing, allowing thousands of short-lived application clients (such as Lambda functions or edge workers) to share a smaller backend connection pool transparently. This is the same client-server architecture with operational glue to make it work at modern serverless scale.
14.3 X Window System: The Famously-Inverted Naming
The X Window System (X11) is a classic illustration of client-server’s directional asymmetry being non-obvious. The X server is the program controlling the display, keyboard, and mouse — running on the user’s workstation. The X client is an application like xterm, Firefox, or Emacs — possibly running on a remote machine. The application asks the display to draw windows; the display tells the application about user input.
Once you understand the asymmetry — the server owns the resource (the display); the client requests services from the resource owner — the naming makes sense. But it confuses every newcomer who expects “server” to mean “the big remote machine.” The X protocol predates the modern shorthand “client-server = remote big machine + local small machine”; it preserves the original abstract definition.
14.3.5 Stripe’s API: Client-Server at API Scale
Stripe’s API is a useful study because it represents the modern best-practice client-server API: REST principles (resource-oriented URLs, standard HTTP methods, predictable status codes), comprehensive documentation, idempotency keys for safe retries, webhooks for server-to-client push, and versioning that has remained backward-compatible since launch. The API serves both Stripe’s own dashboard (a thick browser client) and tens of thousands of merchants’ integrations (some thick clients, some server-to-server automated peers).
A specific design choice worth noting: Stripe uses idempotency keys to make retries safe. A client provides a unique idempotency key with each potentially-retryable request (POST /charges); the server stores the key and the response; a duplicate request with the same key receives the original response without re-executing the operation. This makes the network-failure problem (where the client cannot tell if the server received the request) tractable: the client always retries with the same key, the server deduplicates. This pattern has become an idiom in modern API design and is one of the practical answers to the “what about exactly-once?” question in client-server architectures.
14.4 Discord’s Real-Time Architecture
Discord’s published engineering blog posts describe scaling real-time chat to millions of concurrent connections, and the concrete mechanism is worth knowing precisely. By July 2017 Discord reported “nearly five million concurrent users” with “millions of events per second” flowing through the system (Discord engineering, 2017). The architecture relies on Elixir running on the Erlang virtual machine (the BEAM), whose runtime is famously good at managing very many lightweight processes. When a user connects, they “spin up a session process (a GenServer), which then communicates with remote Erlang nodes that contain guild … processes (also GenServers)” — so every connected user maps to one BEAM process, and a session-VM can hold “up to 500,000 live sessions on it.” Because an average user belongs to about 5 guilds (Discord’s term for a server), the fan-out of a message to a large guild was Discord’s central scaling problem — publishing to a large guild took “anywhere from 900ms to 2.1s” before optimization. Discord built three Elixir libraries to fix this: Manifold (distributed message fan-out), FastGlobal (a read-only shared-heap data store with ~0.3μs lookups, exploiting the BEAM’s constant-pool sharing), and Semaphore (concurrency limiting). By 2019 they pushed past 11 million concurrent users by rewriting the hottest Elixir data structure (a sorted set for the member list) in Rust, bridged into the BEAM through Rustler-built Native Implemented Functions (NIFs), cutting worst-case operation latency from ~640μs in pure Elixir to single-digit microseconds (Discord engineering, 2019).
The architecture is still client-server: the Discord client (desktop app, mobile app, browser) opens a WebSocket to a Discord server cluster; the server holds the conversation state (member presence, message history pointers, typing indicators); the client renders. The reason Discord scales is not that it abandoned client-server but that it chose a runtime tuned to the connection-density problem (one cheap BEAM process per connection rather than one OS thread) and engineered the server side appropriately — guild processes on remote nodes, message fan-out via Manifold, and routing of sessions to backend nodes. The lesson for the interview is precise: “millions of concurrent connections” is not a property of client-server itself but of the connection-density of the runtime chosen to terminate those connections.
15. The CAP Theorem and Client-Server’s Position In It
A topic that recurs in interviews: how does client-server relate to the CAP theorem (Brewer 2000, formally proven by Gilbert & Lynch 2002)? The theorem states that a distributed data store can guarantee at most two of: Consistency (every read sees the most recent write), Availability (every request gets a non-error response), and Partition Tolerance (the system continues operating despite arbitrary network partitions).
A pure single-server client-server system does not engage CAP — there is no partition between server replicas because there is only one server. The trade-off appears the moment the server is replicated for high availability. Now the system is distributed; it has multiple nodes that can disagree. Choices appear:
- CP (Consistency + Partition Tolerance). During a network partition, the system rejects writes (or reads) on the side of the partition that cannot confirm the latest state. ZooKeeper, etcd, and Spanner take this stance — strong consistency is preserved at the cost of unavailability during partitions.
- AP (Availability + Partition Tolerance). During a network partition, both sides of the partition continue accepting reads and writes; consistency is restored later (eventual consistency). Cassandra, Dynamo-style stores, and most NoSQL systems take this stance.
- CA (Consistency + Availability). Only achievable in a non-distributed system, which is to say a single-node client-server system. Once you replicate, partitions are inevitable, and you must pick CP or AP.
This sharpens the engineering meaning of “scale a client-server system to high availability”: you are choosing CP or AP. The original Postgres single-primary architecture is CP-leaning (a network partition isolating the primary from its replicas means writes pause until the partition heals or a manual failover happens). Cassandra’s tunable consistency is AP-leaning (writes succeed at any reachable replica; eventual reconciliation handles inconsistency). Modern cloud databases (Aurora, Spanner, CockroachDB) make different choices in this trade and advertise them explicitly. A confident interview answer can state the system’s CAP positioning rather than pretending the choice doesn’t exist.
A more recent refinement worth knowing: the PACELC theorem (Abadi 2010) extends CAP by observing that even in the absence of a network partition (the “else” branch), a distributed system must choose between latency and consistency. Specifically: if there is a partition (P), availability or consistency must be sacrificed (this is CAP); else, latency or consistency must be sacrificed. A system that prioritizes consistency in normal operation (Spanner, CockroachDB, traditional RDBMS with synchronous replication) has higher latency because every write must coordinate with multiple replicas before acknowledging; a system that prioritizes latency (DynamoDB with eventual consistency, Cassandra default config) has lower latency because writes acknowledge after one replica accepts, but readers may see stale data. PACELC clarifies why “we use a CP database” doesn’t fully describe the system; the latency-vs-consistency choice in the no-partition case is the more frequently-experienced tradeoff.
For interview purposes, the layered understanding is: CAP names three properties (C, A, P) and observes that under partition you can have at most two; PACELC adds that even without partition, latency and consistency are still in tension. The combination tells you most of what you need to know about a distributed system’s design choices: where it falls in the CAP triangle (under partition) and where it falls on the latency-consistency spectrum (in normal operation). Citing PACELC in addition to CAP signals depth of reading that few candidates demonstrate.
16. Summary: The Atom of Distributed Systems
Client-Server is the foundational pattern. Every more advanced distributed-system architecture is, structurally, a composition or refinement of client-server primitives. Microservices are many client-server pairs. Replicated databases are client-server with a leader-follower coordination underneath. Event-driven systems use brokers, which are themselves client-server protocols where the broker is the server. Even peer-to-peer systems decompose, in practice, into rotating client-server interactions per chunk.
The architectural decisions a system designer makes are typically refinements of client-server: synchronous or asynchronous; stateful or stateless server; one server replica or many; in-region or geo-distributed; thin client or thick client; pull or push (where push always requires a client-initiated session underneath). Each refinement is a deliberate choice among the trade-offs surveyed in this note; together they compose the architecture of the system.
The single most important takeaway for interviews: client-server is not “the simple architecture you graduated past.” It is the substrate. Mastering its trade-offs is the precondition for thinking clearly about any of the more elaborate architectures that build on it.
A final framing for the engineer evaluating an architecture: ask first which entity owns the truth? That entity is the server, regardless of physical deployment. Then ask which entity initiates the request? That entity is the client. Once these two questions are answered, the rest of the architectural decisions cascade — what’s the wire protocol, is the server stateful or stateless, how is failover handled, is there a cache between, is the client thick or thin, are there intermediaries (load balancers, gateways, CDNs) — and each is a refinement of the basic client-server pattern, not an alternative to it. The interview question that probes this depth is “walk me through how this system works” — a candidate who reduces every layer to “the server holds the truth, the client requests services, here are the refinements I chose for this workload” is articulating the client-server pattern correctly. A candidate who jumps to specific technologies (Kafka! GraphQL! Kubernetes!) without naming the underlying pattern has missed the substrate.
The discipline of asking these two questions before reaching for technology choices is the difference between architectural thinking and tooling-first design. Many engineers default to the latter, picking trendy technologies and then retrofitting an architecture to them; the senior engineer’s habit is to articulate the client-server semantics first (what state, who owns it, who consumes it, what is the trust model), and only then choose the wire protocol, runtime, and operational topology that fit those semantics. The substrate framing is the discipline.
A final practical heuristic: when designing a system from scratch, write down the minimum viable client-server contract first — one server, one type of client, one wire protocol, one minimal set of operations.
Only then add refinements (caching, replication, push channels, sharding, gateways) one at a time, justifying each by a specific workload requirement. The minimum-viable starting point grounds the design in reality; the refinements grow it deliberately rather than incidentally. Architectures designed top-down from “we need Kubernetes plus Kafka plus a service mesh” routinely produce over-engineered systems where most of the components solve problems the system does not have. Architectures designed bottom-up from the client-server contract produce systems where every component earns its place.
17. See Also
- System Architectures MOC — parent
- SWE Interview Preparation MOC — grandparent
- Layered Architecture (N-Tier) — the natural extension of two-tier client-server into three or more tiers
- Peer-to-Peer Architecture — the symmetric alternative
- Microservices Architecture — many client-server pairs composed into a system
- Master-Worker Architecture — asymmetric variant focused on work distribution
- Leader-Follower Replication Architecture — how a single-server side becomes highly available
- Leaderless Replication Architecture — Dynamo-style alternative
- Sharded Architecture — partitioning the server to scale beyond one machine
- API Gateway System Design — common front to a client-server fleet
- Service Mesh System Design — internal client-server for microservice meshes
- Publish Subscribe System Design — the push-style complement
- Message Queue System Design — asynchronous client-server
- Distributed Tracing System Design — necessary observability when many client-server pairs compose
- Circuit Breaker Pattern — discipline for client failure handling
- Retry with Backoff Pattern — discipline for transient server failures
- Token Bucket — for rate-limiting clients
- Sliding Window Rate Limiter — sibling rate limiter
- Consistent Hashing — for scaling the server side via key partitioning
- Content Delivery Network System Design — pushing client-server’s read path to the edge
- Multi-Region Active-Active Architecture — geographically distributing the server side
- Architecture Decision Records — for documenting why this architecture was chosen