The Client-Server Pattern in Software Architecture
Every time you open a web browser, send an email, or pull up an app on your phone, you are using the client-server pattern. One component asks for something. Another component does the work and sends back a result. That split is so fundamental to how software is built that most developers stop noticing it.
But understanding client-server deeply, not just as a networking concept but as an architecture pattern, changes how you think about code. It is the reason APIs exist. It is the reason you can swap a database without rewriting your application. And it shows up in coding problems more often than you might expect.
What is the client-server pattern?
The client-server pattern separates a system into two roles:
- Client: The component that requests a service. It knows what it wants but does not know how the server accomplishes it.
- Server: The component that provides the service. It hides its internal implementation behind a clean API and responds to client requests.
The key principle is information hiding. The server exposes a small, well-defined interface. The client interacts only through that interface. Everything behind the interface, the data structures, the algorithms, the storage mechanism, is invisible to the client.
This means the server can change its internal implementation completely without the client ever knowing. As long as the API contract stays the same, the client keeps working.
The request-response model
Client-server communication follows a simple cycle:
- The client sends a request to the server.
- The server receives the request and processes it.
- The server sends a response back to the client.
The client waits for the response (in synchronous communication) or continues working and handles the response later (in asynchronous communication). Either way, the flow is the same: request in, response out.
This model is everywhere. Your browser sends an HTTP request to a web server. The server queries a database, builds an HTML page, and sends it back. Your browser renders the page. That entire cycle is one request-response round trip.
Why it works: the core properties
The client-server pattern dominates modern software because of a few powerful properties.
Separation of concerns. The client handles presentation and user interaction. The server handles business logic and data. Each side can focus on what it does best without worrying about the other side's problems.
Information hiding. The server's internals are invisible to the client. The client does not know if the server stores data in a hash map, a B-tree, a file on disk, or a distributed database. It just calls the API and gets results.
Multiple clients, one server. A single server can serve a web app, a mobile app, and a CLI tool all at the same time. Each client talks to the same API. This is why companies build one backend and multiple frontends.
Independent evolution. Because the client and server only share an API contract, they can evolve at different speeds. You can rewrite the server from Python to Go without touching the client. You can rebuild the client in a new framework without touching the server.
A simple example in Python
Here is a key-value store server with a clean API. The client interacts with get and put methods without knowing anything about how data is stored internally.
class KeyValueServer:
"""A server that stores key-value pairs."""
def __init__(self):
self._store = {} # Internal implementation: a dictionary
def get(self, key: str) -> str | None:
"""Client API: retrieve a value by key."""
return self._store.get(key)
def put(self, key: str, value: str) -> None:
"""Client API: store a key-value pair."""
self._store[key] = value
# Client code
server = KeyValueServer()
server.put("user:1", "Alice")
server.put("user:2", "Bob")
print(server.get("user:1")) # "Alice"
print(server.get("user:3")) # None
The client calls put and get. It never accesses _store directly. It does not know or care that _store is a dictionary. All it knows is the API: give me a value for this key, or store this key-value pair.
Swapping the implementation
This is where the pattern pays off. Suppose you decide to move from an in-memory dictionary to a SQLite database. The client code does not change at all.
import sqlite3
class KeyValueServer:
"""Same API, completely different internals."""
def __init__(self, db_path: str = ":memory:"):
self._conn = sqlite3.connect(db_path)
self._conn.execute(
"CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)"
)
def get(self, key: str) -> str | None:
"""Client API: retrieve a value by key."""
row = self._conn.execute(
"SELECT value FROM kv WHERE key = ?", (key,)
).fetchone()
return row[0] if row else None
def put(self, key: str, value: str) -> None:
"""Client API: store a key-value pair."""
self._conn.execute(
"INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)",
(key, value),
)
self._conn.commit()
# Exact same client code works with no changes
server = KeyValueServer()
server.put("user:1", "Alice")
server.put("user:2", "Bob")
print(server.get("user:1")) # "Alice"
print(server.get("user:3")) # None
The implementation went from three lines of dictionary operations to SQL queries, a database connection, and table creation. But the client code at the bottom is identical. That is the power of hiding implementation behind an API.
Real-world examples
The client-server pattern is the backbone of the internet and most software systems.
Web browsers and web servers. Your browser (client) sends HTTP requests. The web server processes them and sends back HTML, CSS, and JavaScript. The browser does not know if the server runs Django, Express, or a custom C++ binary. It just speaks HTTP.
Mobile apps and REST APIs. A mobile app (client) makes API calls to a backend server. The server handles authentication, business logic, and database access. The app only knows the API endpoints and the shape of the JSON responses.
Database clients and database servers. Your application code (client) sends SQL queries to a database server (PostgreSQL, MySQL, etc.). The database handles query planning, indexing, caching, and disk I/O. Your code just sends a query and gets rows back.
Email clients and mail servers. Outlook, Gmail, or Thunderbird (clients) communicate with mail servers using SMTP and IMAP protocols. The client composes and displays messages. The server handles routing, storage, and delivery.
In every case, the pattern is the same: the client says what it wants, the server figures out how to do it, and they communicate through a shared protocol or API.
Client-server in coding problems
Here is something that surprises most students: every "Design X" problem on LeetCode is a client-server problem.
Think about it. The problem gives you an API, the client interface. Your job is to build the server, the internal implementation that makes that API work correctly and efficiently. The LeetCode test harness is the client. It calls your methods and checks the responses. It has no idea what data structures you use internally. It only cares that the API behaves correctly.
LRU Cache
LRU Cache is the purest example. The client API is two methods: get(key) and put(key, value). That is it. The client does not know how you implement eviction. It does not know about your doubly linked list. It does not know about the hash map that maps keys to list nodes.
Your job is to build a server whose internals (hash map plus doubly linked list) make get and put run in O(1) time while correctly evicting the least recently used entry when capacity is exceeded. The client never sees any of that machinery. It just calls the API and expects correct results.
Implement Trie
Implement Trie follows the same structure. The client API is three methods: insert(word), search(word), and startsWith(prefix). Behind that API, the server is a tree of nodes where each node has a dictionary of children and an is_end flag. The client does not know about nodes, children dictionaries, or boolean flags. It just calls the three methods and trusts the server to handle the rest.
The general pattern
All "Design X" problems follow this template:
- The problem defines the client API (the methods you must implement).
- You design the server internals (the data structures and algorithms).
- The test harness acts as the client, calling your methods and verifying responses.
Once you recognize this, design problems stop feeling mysterious. You are not building something alien. You are building a server with a known API contract. Define the interface, then figure out what internals you need to support it efficiently.
Advantages
Clean separation of concerns. The client and server each have a focused responsibility. The client handles what the user sees. The server handles what happens behind the scenes.
Independent scaling. If the server is overloaded, you can add more server instances without changing the client. Horizontal scaling is a direct consequence of this architecture.
Multiple client types. Build one server and connect it to a web app, a mobile app, a CLI tool, and a third-party integration. Each client speaks the same API.
Easier maintenance. Because the client and server only share an API contract, you can refactor, optimize, or rewrite either side independently. A bug in the server does not require changes to the client (as long as the API still behaves correctly).
Disadvantages
Single point of failure. If the server goes down, every client stops working. In a web application, if your backend crashes, no user can access the service. This is why production systems use load balancers, redundancy, and failover strategies.
Network latency. When the client and server run on different machines, every request-response cycle involves network communication. That adds milliseconds (or more) of delay compared to a single-process architecture where everything runs locally.
Server bottleneck. As more clients connect, the server has to handle more concurrent requests. Without careful design (caching, connection pooling, rate limiting), the server can become the bottleneck that limits the entire system's performance.
Variations
Thin client vs. thick client. A thin client does almost nothing locally and relies on the server for all processing (think a web browser rendering server-generated HTML). A thick client handles significant logic locally and only calls the server for data (think a single-page application with a React frontend). The tradeoff is between client simplicity and server load.
Peer-to-peer. In a peer-to-peer architecture, every node acts as both a client and a server. BitTorrent is the classic example: each peer downloads (client behavior) and uploads (server behavior) simultaneously. There is no central server, which eliminates the single point of failure but adds complexity in coordination.
Microservices. Instead of one big server, you split the server into many small, specialized servers. Each microservice handles one domain (authentication, payments, notifications) and they communicate with each other through APIs. Microservices are client-server all the way down. Every service is both a server (to the services that call it) and a client (to the services it depends on).
The takeaway
The client-server pattern is simple in concept and powerful in practice. One side asks, the other side answers, and the implementation details stay hidden behind a clean interface.
That principle applies at every scale. It applies when you design a REST API for a web application. It applies when you implement an LRU Cache on a whiteboard. It applies when you wrap a complex algorithm behind a clean function signature so the rest of your code does not have to think about the messy details.
The best engineers think about interfaces before implementations. They ask "what should the API look like?" before they ask "what data structure should I use?" That instinct, defining the contract first and building the internals second, is the client-server pattern in action.
Related posts
- Software Architecture Patterns in Coding Problems covers client-server alongside pipe-and-filter, master-slave, and layered architecture.
- LRU Cache: Hash Map + Doubly Linked List is the classic client-server design problem, building a server with O(1) get and put.
- Implement Trie: The Prefix Tree is another client-server design problem with insert, search, and startsWith behind a tree of nodes.