Skip to main content

Command Palette

Search for a command to run...

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

Updated
3 min read
How to Think About Backends — Mental models, concurrency, architecture
A
I’m Abass Ibrahim: an IT Support Specialist (Oil & Gas) and software web developer from Lagos, Nigeria. I build fast, clean web apps with React/Next.js, Tailwind, and Node.js, design in Figma, and experiment with Web3 + automation/bots. Here I share what I learn and build.

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/