How to Think About Backends — Mental models, concurrency, architecture

CHAPTER 1 — HOW TO THINK ABOUT BACKENDS
The Mental Model Every Backend Dev Needs
Before writing a single line of code, you need to understand how your server thinks. Most backend bugs and performance problems come from not understanding two things: what kind of work your code is doing, and how the framework handles it.
1.1 I/O-bound vs CPU-bound Work
Every operation your backend does falls into one of two buckets:
I/O-bound — Your code is waiting. Waiting for the database to respond, waiting for a file to load, waiting for an API call to come back. The CPU is idle. Node.js handles this brilliantly.
CPU-bound — Your code is working hard. Hashing a password, resizing an image, parsing a huge JSON file. The CPU is maxed out. Node.js is NOT great at this inline.
RULE: Always use async/await for I/O-bound work. Never run CPU-bound work in the main thread — offload it to Worker Threads.
1.2 async/await — The Right Way
Node.js is single-threaded. If you write blocking code in a request handler, every other request waits. Always make your I/O asynchronous.
✅ DO THIS — runs both database calls at the same time:
const [user, orders] = await Promise.all([
db.users.findById(userId),
db.orders.findByUser(userId)
]);
❌ NEVER DO THIS — runs one after the other, 2x slower:
const user = await db.users.findById(userId);
const orders = await db.orders.findByUser(userId);
Use Promise.all() when two calls don't depend on each other. Use sequential await only when you need the result of the first call to make the second.
1.3 The Layered Architecture
Every backend project you build should be divided into distinct layers. Think of it like a restaurant: the waiter doesn't cook the food, the chef doesn't serve it, and the dishwasher doesn't take orders. Each role has one job.
| Layer | Its Only Job | Real Example |
|---|---|---|
| Routes / Controllers | Receive HTTP requests, call services, send responses. Nothing else. | GET /users → calls UserService.getAll() → returns JSON |
| Middleware | Cross-cutting concerns that run before/after routes. | Auth check, rate limiting, request logging, validation |
| Services | ALL business logic lives here. No req/res objects. | calculateOrderTotal(), sendWelcomeEmail(), validateDiscount() |
| Repositories | ALL database queries live here. No business logic. | findUserById(), createOrder(), updateProductStock() |
| Database | PostgreSQL, Redis — stores your data. | Tables, indexes, migrations |
WHY: When each layer has a single responsibility, you can test them in isolation, swap databases without touching business logic, and debug faster because you always know where to look.
1.4 Standard Directory Structure
Use this folder layout on every project. Consistency means any developer can open your project and instantly know where everything is.
src/
├── routes/ → HTTP wiring only (which URL calls what)
├── controllers/ → Receive request, call service, send response
├── middlewares/ → Auth, logging, rate limiting, validation
├── services/ → ALL business logic (no req/res here!)
├── repositories/ → ALL database queries (only queries!)
├── models/ → Type definitions and schema references
├── utils/ → Shared helpers and constants
├── config/ → Database, Redis, env config
├── jobs/ → Background job processors
├── migrations/ → Database migration files
└── tests/
├── unit/
├── integration/
└── fixtures/
