System design is how the individual parts of your app — your code, database, cache, queue, and APIs — fit together into something that's fast, reliable, and able to grow. Every app makes these choices, even simple ones, usually without realising it.
If Cartara flagged this in your diff, you likely added or modified a significant architectural piece: a caching layer, a queue, a new service, or infrastructure configuration.
What System Design Actually Means
Any single component is easy to understand on its own. System design is the harder question of how they combine: where requests flow, where data lives, what happens under load, and what breaks when one part fails.
The recurring theme is trade-offs. There's no single "best" architecture — only the one that fits your requirements for speed, cost, complexity, and scale. Good design is choosing the right trade-offs for your situation, not copying what a company 1,000× your size built.
The Anatomy of a Typical Web System
Most web applications are arrangements of the same handful of parts:
```
Users
│
[Load Balancer] ← spreads traffic across app servers
│
[App Servers] ×N ← your code; stateless so any server handles any request
│ │
[Cache] [Database] ← fast reads from cache; source of truth in the DB
│
[Queue] → [Workers] ← slow/background jobs handled out of band
│
[Object Storage] ← files, images, uploads (e.g. S3)
```
You don't start with all of this. You add pieces as real needs appear. But this is the shape most systems converge toward.
The Core Building Blocks
Load Balancer
Sits in front of multiple app servers and distributes incoming requests. It's what lets you run more than one server (for capacity and redundancy) behind a single address. If one server fails, the balancer routes around it automatically.
Stateless App Servers
The key trick that makes horizontal scaling possible: app servers keep no important state in memory between requests. Sessions, uploads, and cached values live in shared services — not on any one server. Because every server is interchangeable, you can add or remove them freely. This is also why containers and serverless functions work the way they do.
Database
The source of truth for your app's data. Usually the first thing to become a bottleneck as you grow, which is why indexing and connection pooling matter. See Database Fundamentals.
Cache
A fast in-memory layer (typically Redis) in front of slow operations, so repeated reads don't hit the database every time. See Caching Fundamentals.
Queue and Workers
For work that's slow or doesn't need to finish before the user gets a response — sending emails, processing uploads, calling a slow AI model — push a job onto a queue and let background workers handle it. This keeps user-facing requests fast and absorbs traffic spikes. See Message Queues and Async Processing.
Object Storage
Files (images, PDFs, uploads) don't belong in your database. Store them in object storage (AWS S3, Supabase Storage, Cloudflare R2) and keep just the URL in the database.
Synchronous vs. Asynchronous
A foundational distinction. Synchronous work happens while the user waits — they get the answer in the response. Asynchronous work is handed off to happen later, and the user is told "we're on it."
The skill is knowing which is which. Looking up a user profile? Synchronous. Encoding a video, generating a report, sending 10,000 emails? Asynchronous — making the user wait for those is how requests time out and systems fall over under load.
Three Ideas That Shape Every Design
Latency vs. Throughput
Latency is how long one request takes. Throughput is how many requests you can handle per second. They're different goals — optimising one sometimes costs the other. Know which one your users actually care about.
Single Points of Failure
Any component with no backup will eventually take your whole system down when it fails. Redundancy — multiple app servers, database replicas, multiple availability zones — removes single points of failure. You don't need full redundancy on day one, but you should know where your single points of failure are.
Design for Failure
Mature systems assume parts will fail and degrade gracefully:
- Timeouts — never wait forever on another service
- Retries with backoff — retry a failed call, but wait progressively longer so you don't overwhelm a struggling service
- Circuit breakers — if a dependency is clearly down, stop calling it rather than piling on failed requests
- Graceful degradation — if recommendations are unavailable, show the page without them rather than failing the whole request
What You'll See in Your Code
Architectural decisions show up in diffs as:
Adding a cache:
```ts
// Check cache before hitting the database
const cached = await redis.get(user:${userId});
if (cached) return JSON.parse(cached);
const user = await db.users.findById(userId);
await redis.setex(user:${userId}, 3600, JSON.stringify(user));
return user;
```
Pushing a background job:
```ts
// Don't send the email in the request handler — queue it
await queue.add('send-welcome-email', { userId, email });
res.json({ success: true }); // respond immediately
```
Infrastructure config (e.g., docker-compose.yml, railway.toml) adding new services like Redis or a worker process.
Resist the Urge to Over-Build
The most common system design mistake at small scale is building for imaginary scale: microservices, sharding, and elaborate queues for an app with 50 users. Complexity has a real ongoing cost in time, debugging, and maintenance.
Start with the simplest thing that works — one app, one database, a managed host. Add a cache when reads get heavy. Add a queue when you have genuinely slow background work. Let real problems pull new components into your system rather than adding them speculatively.
A boring architecture you fully understand beats a clever one you don't.