.png)
7 System Design Tradeoffs Every Developer Should Know
Every architectural decision you make as a developer is really a tradeoff in disguise. Do you scale up or scale out? Should the server remember the client or forget it the moment the response is sent? Do you make the client wait, or let it move on and notify it later?
There's rarely a universally "correct" answer — only the right answer for your system's constraints. Understanding these tradeoffs deeply is what separates developers who can design systems from developers who can only follow patterns they've memorized.
Here are 7 foundational tradeoffs that show up again and again in real-world backend and distributed systems work.
1. Push vs Pull Model
Push — the server sends data to clients the moment it's available. Pull — clients periodically ask the server if there's anything new.
Push feels more "real-time" and efficient because there's no wasted polling — but it requires the server to track every connected client and manage delivery, retries, and backpressure. Pull is simpler and more resilient (a client can just retry on its own schedule), but it wastes resources when there's nothing new, and introduces latency between when data becomes available and when the client finds out.
Where you see it: Webhooks (push) vs. cron-based sync jobs (pull); WebSocket notifications (push) vs. REST polling dashboards (pull).
The core question: Can you afford the complexity of tracking every client's state, or is occasional staleness an acceptable cost for simplicity?
.png)
2. Concurrency vs Parallelism
Concurrency — dealing with multiple tasks at the same time, without necessarily executing them simultaneously. One thread juggling many tasks, making progress on each in small steps (common with I/O-bound work).
Parallelism — literally executing multiple tasks at the exact same instant, using multiple CPU cores.
These two get conflated constantly, but they solve different problems. Concurrency is about structure — how you organize work so nothing blocks everything else. Parallelism is about throughput — using more hardware to do more work in less time.
Where you see it: An event loop handling thousands of open connections (concurrency) vs. a data pipeline splitting a big dataset across CPU cores to process it faster (parallelism).
The core question: Are you I/O-bound (waiting on network/disk) or CPU-bound (waiting on computation)? That answer tells you which one actually helps.
.png)
3. Vertical vs Horizontal Scaling
Vertical scaling — add more power (CPU, RAM) to a single machine. Horizontal scaling — add more machines and distribute the load across them.
Vertical scaling is simple — no distributed systems problems, no load balancing, no data partitioning. But it has a hard ceiling: eventually you run out of hardware to buy, and a single point of failure remains. Horizontal scaling has no real ceiling and improves fault tolerance, but it introduces real complexity — network calls between nodes, data consistency, load balancing, and coordination.
Where you see it: Upgrading a database server's instance size (vertical) vs. adding read replicas or sharding across nodes (horizontal).
The core question: Is your bottleneck something a bigger machine solves, or does your system need to survive machine failure and grow indefinitely?
.png)
4. Stateful vs Stateless Architecture
Stateless — the server holds no client session data between requests. Every request carries everything the server needs (like a JWT) to process it independently.
Stateful — the server remembers previous interactions, often tying a client to a specific server via a "sticky session."
Stateless services are easier to scale horizontally — any server can handle any request, so you can add or remove instances freely. Stateful services can be more efficient for the immediate interaction (no need to look up or re-send context) but they complicate load balancing, failover, and scaling since a client is bound to a specific server holding its state.
Where you see it: Token-based auth (JWT) in modern REST APIs (stateless) vs. traditional session-based logins stored in server memory (stateful).
The core question: Do you need every server to be interchangeable, or is binding a client to one server an acceptable (or even necessary) tradeoff?
.png)
5. Long Polling vs WebSockets
Long Polling — the client sends a request, and the server holds it open until data is available, then responds. The client immediately sends another request, repeating the cycle.
WebSockets — establish a single persistent, full-duplex TCP connection, so both client and server can send data to each other at any time without re-establishing a connection.
Long polling works over plain HTTP, so it plays nicely with existing infrastructure like load balancers and proxies without special configuration. But it's less efficient — repeated connection setup, held-open requests tying up server resources. WebSockets are far more efficient for real-time, bidirectional communication, but require persistent connection management, and your infrastructure (load balancers, proxies) needs to support it properly.
Where you see it: Older chat implementations and simple notification systems (long polling) vs. live collaborative editors, multiplayer games, and trading platforms (WebSockets).
The core question: Is the interaction occasional and one-directional-ish, or is it truly real-time and bidirectional?
.png)
6. Strong vs Eventual Consistency
Strong Consistency — guarantees that after a write, any subsequent read from any node returns the updated value, immediately.
Eventual Consistency — allows temporary divergence between nodes; they will all converge to the same value eventually, but not immediately.
This is the classic CAP theorem tradeoff in disguise. Strong consistency gives you predictability and correctness guarantees that make reasoning about your system easier — but it comes at the cost of latency and availability, since nodes need to coordinate before confirming a write. Eventual consistency sacrifices that immediate guarantee in exchange for lower latency, higher availability, and better partition tolerance.
Where you see it: Traditional relational databases with synchronous replication (strong) vs. DNS propagation or DynamoDB's default read behavior (eventual).
The core question: Can your system tolerate a brief window where different users see different data, in exchange for speed and availability?
.png)
7. Synchronous vs Asynchronous Communication
Synchronous — the caller sends a request and blocks, waiting for the response before continuing.
Asynchronous — the caller sends a request and continues working, handling the response (via callback or event) whenever it arrives.
Synchronous communication is easier to reason about — the flow is linear, and error handling happens right where the call was made. But it means your caller is idle and vulnerable to cascading slowdowns if a downstream service is slow. Asynchronous communication decouples services and improves resilience and throughput — a slow downstream service doesn't block everything upstream — but it introduces complexity: you now need to handle out-of-order responses, retries, and eventual failure notification.
Where you see it: A direct REST API call that returns a result immediately (synchronous) vs. a message queue-based pipeline where a worker processes a job and emits an event when done (asynchronous).
The core question: Does the caller genuinely need the result right now, or can it keep working while the response is handled later?
.png)
The pattern behind all seven
Notice that every single one of these tradeoffs follows the same shape: simplicity and immediacy on one side, scale and resilience on the other. There's no universally better option — only the option that fits your system's actual constraints: your traffic patterns, your consistency requirements, your team's operational maturity, and how much complexity you can afford to own.
The goal isn't to memorize which side is "correct." It's to recognize which tradeoff you're facing, understand what you're gaining and giving up on each side, and make that decision deliberately instead of by default.
Found this useful? I write about system design and backend engineering regularly — follow along @husnainIzhar for more.