AlgoInsight

Free lesson · System Design Fundamentals

Client-Server Architecture

Hard9 min readsystem-designhlddistributed-systems

Lesson 24.1 - Client-Server Architecture

The first lesson of Module 24.

Before we discuss monoliths, microservices, serverless, or event-driven patterns, we need to start with the pattern that underlies almost all of them: client-server. It's so fundamental that most engineers don't even think of it as an architectural choice - it's just "how the internet works." But it IS a choice, and understanding it as a choice (with alternatives, like peer-to-peer - 24.6) deepens your understanding of every pattern that builds on top of it.

Client-server architecture divides a system into two roles: clients that request services, and servers that provide them. The client initiates communication (sends a request). The server waits for requests, processes them, and sends responses. The client depends on the server; the server does not depend on any specific client. The relationship is asymmetric: many clients connect to one server (or a cluster of servers), but clients don't communicate with each other directly (they communicate through the server).

Every web application, mobile app backend, REST API, database query, DNS lookup, and email system uses client-server architecture. When your browser loads google.com, your browser is the client and Google's web server is the server. When your application queries PostgreSQL, your application is the client and PostgreSQL is the server. When your phone opens Instagram, the Instagram app is the client and Instagram's backend is the server.

The Architecture

flowchart TD
    subgraph Clients["CLIENTS (many)"]
        C1["Web Browser"]
        C2["Mobile App"]
        C3["CLI Tool"]
        C4["Another Service"]
    end
    
    subgraph Server["SERVER (one or cluster)"]
        LB["Load Balancer"]
        S1["Server Instance 1"]
        S2["Server Instance 2"]
        S3["Server Instance 3"]
        DB["Database"]
        
        LB --> S1 & S2 & S3
        S1 & S2 & S3 --> DB
    end
    
    C1 -->|"HTTP request"| LB
    C2 -->|"HTTP request"| LB
    C3 -->|"gRPC call"| LB
    C4 -->|"REST API call"| LB

    style Clients fill:#a78bfa,stroke:#8b5cf6,color:#000
    style Server fill:#34d399,stroke:#10b981,color:#000
A client-server architecture diagram. On the left: four types of clients - a web browser, a mobile app, a CLI tool, and another service (acting as a client to this server). Each client sends requests to a load balancer. The load balancer distributes requests across three server instances (horizontal scaling). All server instances connect to a shared database. The clients are many and diverse - any device or service that speaks the server's protocol (HTTP, gRPC, WebSocket) can be a client. The server is centralized - it owns the data, enforces business rules, and responds to client requests. The communication is always initiated by the client. The server never spontaneously pushes data to a client (unless using WebSockets or server-sent events - which are still established by a client-initiated connection). This asymmetry - clients request, servers respond - is the defining characteristic of client-server architecture.

Why Client-Server Dominates

Interactive animation: Five things the asymmetry buys: one source of truth, one place enforcing rules, simple clients, separate evolution, and scaling.

1. Centralized authority. The server is the single source of truth. When two clients disagree on the state of the data (client A thinks the balance is

00, client B thinks it's
50), the server's database is authoritative. This eliminates the consistency problems that plague decentralized architectures (P2P - 24.6). For any system that requires correctness - financial transactions, inventory management, user authentication - centralized authority is essential.

2. Security boundary. The server controls access. Clients authenticate against the server (JWT, OAuth, session cookies). The server enforces authorization (user X can read this resource but not write it). The server validates all input (preventing SQL injection, XSS, CSRF). In a peer-to-peer system, every peer must independently handle authentication, authorization, and input validation - enormously increasing the attack surface.

3. Simplified client development. The client doesn't need to store data durably, handle concurrent access from other users, or maintain consistency with other clients. It sends requests to the server and renders the response. This makes client code simpler - a mobile app developer doesn't need to implement conflict resolution, distributed locking, or database replication. The server handles all that complexity behind the API.

4. Independent evolution. The client and server can be developed, deployed, and scaled independently - as long as the API contract (the request/response format) is maintained. The server team can rewrite the backend from Python to Go without changing the mobile app. The mobile team can redesign the UI without changing the server. The API is the contract that decouples the two.

5. Horizontal scalability. The server side can scale horizontally: add more server instances behind the load balancer. Each instance handles a fraction of the requests. Clients are unaware of how many server instances exist - they connect to the load balancer's address. This is the scaling model that powers every high-traffic web application: Netflix (hundreds of service instances), Google (thousands), Amazon (millions of server instances across hundreds of services).

Client-Server Communication Patterns

Interactive animation: Request-response, fire-and-forget, polling and an open streaming connection - four ways client and server can talk.

Request-Response (Synchronous)

The client sends a request and waits for the server's response. The client is blocked until the response arrives (or a timeout fires). This is the default pattern for HTTP/REST, gRPC unary calls, and database queries.

Client                    Server
  |--- GET /users/42 ------>|
  |                         | (query DB, build response)
  |<--- 200 OK + JSON ------|
  |                         |

When to use: when the client needs the result before proceeding. "Show the user their profile" - the client can't render the profile until the server sends the data.

Limitation: if the server takes 5 seconds to respond (slow database, expensive computation), the client is blocked for 5 seconds - the UI freezes (in synchronous code) or a spinner spins (in async code with UI feedback). Long-running operations should use asynchronous patterns instead.

Fire-and-Forget (Asynchronous)

The client sends a request and doesn't wait for the result. The server processes the request in the background. The client receives an immediate acknowledgment ("accepted - processing in background").

Client                    Server                    Worker
  |--- POST /reports ------>|                         |
  |<--- 202 Accepted -------|                         |
  |                         |--- queue job ---------->|
  |                         |                         | (generate report)
  |--- GET /reports/42 ---->|                         |
  |<--- 200 + status:done --|                         |

When to use: long-running operations where the client doesn't need the result immediately. "Generate a PDF report" - the client doesn't need to wait 30 seconds for the report to generate. It gets a 202 Accepted immediately and polls for the result later (or receives a webhook/notification when it's ready).

Streaming (Server Push)

The server sends data to the client continuously - either in response to a client request (server-streaming) or bidirectionally (the client and server both send messages - bidirectional streaming).

Server-Sent Events (SSE): the client opens an HTTP connection, and the server sends events as they occur. One-directional (server → client only). Used for: real-time dashboards, live notifications, stock tickers, live sports scores.

WebSockets: a full-duplex (bidirectional) connection. Both client and server can send messages at any time after the initial handshake. Used for: chat applications, collaborative editing, real-time gaming, live trading platforms.

gRPC streaming: supports client-streaming (client sends multiple messages, server responds once), server-streaming (client sends one request, server sends multiple responses), and bidirectional streaming.

Long Polling

The client sends a request. The server holds the request open (doesn't respond) until new data is available or a timeout expires. If data arrives during the hold: the server responds immediately with the data. If the timeout expires: the server responds with "no new data" and the client immediately sends a new long-polling request.

Used by: Slack (before their WebSocket migration), many real-time apps that need push-like behavior without WebSocket infrastructure.

Trade-off vs WebSockets: long polling is simpler (standard HTTP - works through all proxies, firewalls, and load balancers) but less efficient (each response requires a new request - more HTTP overhead). WebSockets are more efficient (persistent connection, minimal per-message overhead) but require WebSocket-aware infrastructure (load balancers, proxies, CDNs).

Tiers in Client-Server Architecture

Interactive animation: The same save request walks through each tier layout, showing how concerns split across machines as a system grows.

Two-Tier (Client + Server)

The client communicates directly with the server, which contains BOTH the application logic AND the database. Earliest web applications used this model: the web server runs PHP/Python/Ruby, queries a database on the same machine, and returns HTML.

When it works: small applications, prototypes, single-developer projects, internal tools. The simplicity of a single server (application + database on one machine) is a genuine advantage for applications that don't need to scale beyond one machine.

Three-Tier (Client + Application Server + Database)

The most common production architecture. The client communicates with the application server (which contains business logic), which communicates with the database server (which stores data). The application server and database are on separate machines - enabling independent scaling and fault isolation.

flowchart LR
    Client["Client\n(Browser / Mobile)"]
    App["Application Server\n(Business logic,\nAPI handlers,\nauthentication)"]
    DB["Database Server\n(PostgreSQL,\nMySQL, MongoDB)"]
    
    Client -->|"HTTP/gRPC"| App -->|"SQL/Protocol"| DB

    style Client fill:#a78bfa,stroke:#8b5cf6,color:#000
    style App fill:#fbbf24,stroke:#f59e0b,color:#000
    style DB fill:#34d399,stroke:#10b981,color:#000
A three-tier architecture diagram. The client (browser or mobile app) communicates with the application server via HTTP or gRPC. The application server contains the business logic: API handlers, authentication, authorization, input validation, and response formatting. The application server communicates with the database server via SQL or a database-specific protocol. The three tiers are on separate machines: the client is the user's device, the application server is a cloud VM or container, and the database is a managed service (RDS, Cloud SQL) or a dedicated database server. Separating the application server from the database enables independent scaling: if the application layer is CPU-bound, add more application server instances. If the database is the bottleneck, scale the database independently (read replicas, sharding). Separating also provides fault isolation: an application server crash doesn't corrupt the database, and a database failover doesn't crash the application servers.

N-Tier (Multiple Intermediate Layers)

Larger systems add additional tiers: a caching layer (Redis, Memcached) between the application server and database, a CDN between the client and application server, a message queue (Kafka, SQS) for asynchronous processing, a search engine (Elasticsearch) for full-text queries, and an API gateway for routing, rate limiting, and authentication.

Each tier adds latency (one more network hop) but provides a specific capability (caching, content delivery, asynchronous processing, search) that the system needs at scale. The principle: add tiers only when a specific performance or functionality problem demands it. Every tier is a new failure mode, a new thing to monitor, and a new thing to deploy. Premature addition of tiers is over-engineering; delayed addition of tiers is a scaling crisis. The skill is knowing when each tier earns its keep.

Client-Server vs Other Patterns

Dimension Client-Server Peer-to-Peer (24.6) Serverless (24.4)
Authority Centralized (server) Distributed (every peer) Centralized (cloud provider)
Scaling Scale the server (horizontal - add instances) Scale automatically (every new peer adds capacity) Scale automatically (cloud scales functions)
Data consistency Server is source of truth Consensus or eventual consistency needed Server (managed DB) is source of truth
Offline capability Limited (client depends on server) Good (peers can work independently) Very limited (functions need the cloud)
Cost at rest Server runs continuously (cost even when idle) No central server cost Zero cost when idle (pay per invocation)
Example Web app, mobile app backend, REST API BitTorrent, blockchain, WebRTC video calls AWS Lambda + API Gateway + DynamoDB

Client-server is the default. Start with client-server unless you have a specific reason to use an alternative (P2P for decentralization, serverless for cost optimization on intermittent workloads). Most system design interview answers assume client-server - what varies is the server-side architecture (monolith, microservices, event-driven).

Client Types and the "Thin Client vs Thick Client" Spectrum

Interactive animation: How much work the client does: central and consistent with a thin client, or local and instant with a thick one.

Thin client: the client does minimal processing - it renders HTML sent by the server (traditional server-rendered web pages), or displays data fetched from the server with minimal transformation. Examples: server-side rendered PHP pages, simple CRUD admin panels.

Thick client (rich client): the client does significant processing - rendering a complex UI framework (React, Flutter), managing local state, caching data, handling offline mode, executing business logic (form validation, computed fields). Examples: single-page applications (SPAs), native mobile apps, desktop applications.

The trend: the industry has moved toward thicker clients over the past 15 years. SPAs (React, Vue, Angular) run complex JavaScript in the browser. Mobile apps cache data locally and work offline (partially). The server is increasingly a "data API" rather than an "HTML generator." This shifts complexity from server to client - reducing server load but increasing client-side development complexity and creating challenges for SEO, initial load time, and accessibility.

The counter-trend: server-side rendering (SSR) frameworks (Next.js, Remix, Nuxt) move rendering back to the server for initial page loads (better SEO, faster first paint) while hydrating to a rich client-side experience after the initial render. This is a hybrid approach - neither purely thin nor purely thick.

Six Takeaways from This Lesson

  1. Client-server divides a system into two asymmetric roles: clients request, servers respond. The server is the centralized authority - it owns data, enforces rules, and controls access.

  2. Communication patterns: request-response (synchronous, most common), fire-and-forget (asynchronous for long operations), streaming (WebSocket/SSE for real-time), long polling (push-like behavior over HTTP).

  3. Three-tier is the standard: client → application server → database. Each tier scales independently. Add more tiers (cache, CDN, queue) only when specific problems demand them.

  4. Client-server enables independent evolution: the API contract decouples client and server development. Change the backend language, change the UI framework - as long as the API stays the same.

  5. Horizontal scaling on the server side is the primary scaling mechanism: add instances behind a load balancer. Clients are unaware of the number of server instances.

  6. Client-server is the default architectural choice. Start here unless decentralization (P2P), zero-idle-cost (serverless), or another specific requirement drives a different choice. Most system design interview answers assume client-server.

What's Next

Lesson 24.2 covers Monolithic Architecture - the most dismissed and most underrated pattern in modern engineering. When a single deployable unit is the right choice, and why most startups should start with a monolith.

Back to System Design Fundamentals