Part 01 · The Backend Mindset
What Is a Backend System?
The jobs a backend quietly does behind every screen: storing, deciding, computing and protecting.
⏱ 1 hours · Station 3 of 153
You tapped "Send". What actually happened?
You type a message to a friend and tap Send. A second later, it shows up on their phone, in another city, even though their phone was asleep in a pocket when you sent it.
Your phone didn't find their phone. It doesn't know where it is. It didn't wait for it to wake up. Something else did all of that: it checked that you were really you, decided you were allowed to message this person, stored the message so it wouldn't be lost, and delivered it when their phone came back online.
That "something else" is the backend. You never see it, and it touches almost everything you do in an app. This lesson is about what it actually does.
The problem: the screen can't do it alone
Last lesson drew the line between the client and the server. So why not let the client do everything? Try it, and four walls appear.
| What the app needs | Why the client alone can't do it |
|---|---|
| Remember things after the app closes, on every device | A phone can be lost, wiped or swapped. Your laptop can't see what's on your phone |
| Enforce rules like "free plan: 3 notes" or "one vote per person" | Anything the client checks, the user can change (lesson 2) |
| Do heavy or shared work: search a million products, run a payment, send an email | A phone has little battery, little memory, and no access to other people's data or the payment network |
| Know who's who, and keep each person's data private | A client can claim to be anyone |
A backend system is the part of the software that lives on machines you control and does exactly those four jobs for every client, all the time.
Think of a bank branch
At a bank, you stand at the counter: that's the client. Behind the counter is everything that makes a bank a bank.
- The teller checks your ID before anything else. (protect)
- They check the rules: is there enough money, are you over the daily limit? (decide)
- They work out the new balance and the fee. (compute)
- They write it into the ledger, which will still be there tomorrow, at every branch. (store)
You never see the vault or the ledger, and you don't need to. You see a counter and a receipt.
Where the analogy stops working. A teller serves one person at a time and goes home at 5 pm. A backend serves thousands of clients in the same second, never closes, and is usually many identical copies running side by side. It also has no eyes: "checking your ID" means checking a token that arrived with the request.
The building blocks
Every backend, from a hobby project to a bank, does four jobs. Most requests touch all four, in this order.
- Protect: who is asking? Every request carries some proof of identity (a session cookie, a token). The backend checks it before doing anything else, and refuses strangers. (Module 9.)
- Decide: is this allowed? The business rules: plan limits, permissions, stock, opening hours. This is the logic that makes your product your product. (Lesson 8.)
- Compute: work something out. Totals, word counts, search results, recommendations, a resized photo. Sometimes tiny, sometimes the expensive part.
- Store: remember it. Save the result somewhere that survives restarts and is shared by everyone: a database, a file store, a cache. (Module 6.)
On top of those four, real backends also talk to other systems: payment providers, email and SMS services, other backends. To the backend, those are just more servers it is a client of.
The whole backend can be described as one function:
response = handle(request, everything the backend remembers)
The client sends the request. The backend brings the memory.
A free-plan user already has 3 notes. Their app is old and still shows the 'Add note' button. They tap it. What should happen?
Behind the scenes: one note, four jobs
Alice adds a note that says "buy milk". Here is everything the backend does with that one request.
One request through the backend
Step 1/6 The app sends the request
The request carries three things: Alice's token, the action (add), and the text. Nothing else: no user id, no count of her notes.
- token
- t-alice
- action
- add
- text
- buy milk
The picture to keep in your head
Picture the backend as a short corridor with four doors. Every request walks it from left to right:
protect → decide → compute → store → response
- A request can be turned back at any door, and the earlier the better: refusing a stranger at the first door costs nothing, while refusing them after an expensive computation wastes work.
- The cheap checks (who, allowed) come before the expensive work (compute, store).
- Only the backend holds the keys to the last room. Clients never write to the store directly.
When you read any backend code, try to find the four doors. They're often spread across middleware, handlers, services and repositories (Module 5), but they're always there.
Try it: which job is this?
Each card is something a real backend does. Which of the four jobs is it?
Protect, decide, compute or store?
401 token expired
Code from first principles
A whole backend in one function, with no network and no database yet. handle takes a request (a token, an action, some text), does the four jobs, and returns a response. Its memory is two maps: which token belongs to whom, and everyone's notes.
The program then sends it eight requests, the way eight different clients might, and prints which job had the final say on each.
package main
import (
"fmt"
"strings"
)
const freeLimit = 3 // the free plan keeps at most 3 notes
// The backend's memory: who each token belongs to, and everyone's notes.
var (
tokens = map[string]string{"t-alice": "alice", "t-bob": "bob"}
notes = map[string][]string{}
)
// handle is the whole backend: one request in, one response out.
// It returns the status, the reply, and which of the four jobs had the final say.
func handle(token, action, text string) (int, string, string) {
user, ok := tokens[token] // protect: who is asking?
if !ok {
return 401, "who are you?", "protect"
}
switch action {
case "add":
if len(notes[user]) >= freeLimit { // decide: is this allowed?
return 403, fmt.Sprintf("free plan keeps %d notes", freeLimit), "decide"
}
words := len(strings.Fields(text)) // compute: work something out
notes[user] = append(notes[user], text) // store: remember it for next time
return 201, fmt.Sprintf("saved note %d (%d words)", len(notes[user]), words), "store"
case "list":
total := 0
for _, n := range notes[user] { // compute over what was stored
total += len(strings.Fields(n))
}
return 200, fmt.Sprintf("%d notes, %d words", len(notes[user]), total), "compute"
}
return 400, "unknown action", "decide"
}
func main() {
requests := [][3]string{
{"", "add", "buy milk"},
{"t-alice", "add", "buy milk"},
{"t-alice", "add", "call mom tonight"},
{"t-alice", "add", "fix the bike"},
{"t-alice", "add", "learn go"},
{"t-bob", "list", ""},
{"t-alice", "list", ""},
{"t-eve", "list", ""},
}
fmt.Printf("%-8s %-5s %-17s %-6s %-26s %s\n", "token", "do", "text", "status", "reply", "job")
for _, r := range requests {
status, reply, job := handle(r[0], r[1], r[2])
token := r[0]
if token == "" {
token = "(none)"
}
fmt.Printf("%-8s %-5s %-17s %-6d %-26s %s\n", token, r[1], r[2], status, reply, job)
}
}const FREE_LIMIT = 3; // the free plan keeps at most 3 notes
// The backend's memory: who each token belongs to, and everyone's notes.
const tokens = { "t-alice": "alice", "t-bob": "bob" };
const notes = {};
const words = (text) => text.split(/\s+/).filter(Boolean).length;
// handle is the whole backend: one request in, one response out.
// It returns the status, the reply, and which of the four jobs had the final say.
function handle(token, action, text) {
const user = tokens[token]; // protect: who is asking?
if (!user) return [401, "who are you?", "protect"];
const mine = (notes[user] ??= []);
if (action === "add") {
if (mine.length >= FREE_LIMIT) return [403, `free plan keeps ${FREE_LIMIT} notes`, "decide"]; // decide: is this allowed?
const count = words(text); // compute: work something out
mine.push(text); // store: remember it for next time
return [201, `saved note ${mine.length} (${count} words)`, "store"];
}
if (action === "list") {
const total = mine.reduce((sum, n) => sum + words(n), 0); // compute over what was stored
return [200, `${mine.length} notes, ${total} words`, "compute"];
}
return [400, "unknown action", "decide"];
}
const requests = [
["", "add", "buy milk"],
["t-alice", "add", "buy milk"],
["t-alice", "add", "call mom tonight"],
["t-alice", "add", "fix the bike"],
["t-alice", "add", "learn go"],
["t-bob", "list", ""],
["t-alice", "list", ""],
["t-eve", "list", ""],
];
const row = (a, b, c, d, e, f) => `${a.padEnd(8)} ${b.padEnd(5)} ${c.padEnd(17)} ${String(d).padEnd(6)} ${e.padEnd(26)} ${f}`;
console.log(row("token", "do", "text", "status", "reply", "job"));
for (const [token, action, text] of requests) {
const [status, reply, job] = handle(token, action, text);
console.log(row(token || "(none)", action, text, status, reply, job));
}use std::collections::HashMap;
const FREE_LIMIT: usize = 3; // the free plan keeps at most 3 notes
// The backend's memory: who each token belongs to, and everyone's notes.
struct Backend {
tokens: HashMap<&'static str, &'static str>,
notes: HashMap<&'static str, Vec<String>>,
}
impl Backend {
// handle is the whole backend: one request in, one response out.
// It returns the status, the reply, and which of the four jobs had the final say.
fn handle(&mut self, token: &str, action: &str, text: &str) -> (u16, String, &'static str) {
let Some(&user) = self.tokens.get(token) else {
return (401, "who are you?".into(), "protect"); // protect: who is asking?
};
let mine = self.notes.entry(user).or_default();
match action {
"add" => {
if mine.len() >= FREE_LIMIT {
return (403, format!("free plan keeps {FREE_LIMIT} notes"), "decide"); // decide: is this allowed?
}
let words = text.split_whitespace().count(); // compute: work something out
mine.push(text.to_string()); // store: remember it for next time
(201, format!("saved note {} ({words} words)", mine.len()), "store")
}
"list" => {
let total: usize = mine.iter().map(|n| n.split_whitespace().count()).sum(); // compute over what was stored
(200, format!("{} notes, {total} words", mine.len()), "compute")
}
_ => (400, "unknown action".into(), "decide"),
}
}
}
fn main() {
let mut backend = Backend {
tokens: HashMap::from([("t-alice", "alice"), ("t-bob", "bob")]),
notes: HashMap::new(),
};
let requests = [
("", "add", "buy milk"),
("t-alice", "add", "buy milk"),
("t-alice", "add", "call mom tonight"),
("t-alice", "add", "fix the bike"),
("t-alice", "add", "learn go"),
("t-bob", "list", ""),
("t-alice", "list", ""),
("t-eve", "list", ""),
];
println!("{:<8} {:<5} {:<17} {:<6} {:<26} job", "token", "do", "text", "status", "reply");
for (token, action, text) in requests {
let (status, reply, job) = backend.handle(token, action, text);
let shown = if token.is_empty() { "(none)" } else { token };
println!("{:<8} {:<5} {:<17} {:<6} {:<26} {}", shown, action, text, status, reply, job);
}
}FREE_LIMIT = 3 # the free plan keeps at most 3 notes
# The backend's memory: who each token belongs to, and everyone's notes.
tokens = {"t-alice": "alice", "t-bob": "bob"}
notes = {}
def handle(token, action, text):
"""The whole backend: one request in, one response out.
Returns the status, the reply, and which of the four jobs had the final say."""
user = tokens.get(token) # protect: who is asking?
if user is None:
return 401, "who are you?", "protect"
mine = notes.setdefault(user, [])
if action == "add":
if len(mine) >= FREE_LIMIT: # decide: is this allowed?
return 403, f"free plan keeps {FREE_LIMIT} notes", "decide"
words = len(text.split()) # compute: work something out
mine.append(text) # store: remember it for next time
return 201, f"saved note {len(mine)} ({words} words)", "store"
if action == "list":
total = sum(len(n.split()) for n in mine) # compute over what was stored
return 200, f"{len(mine)} notes, {total} words", "compute"
return 400, "unknown action", "decide"
requests = [
("", "add", "buy milk"),
("t-alice", "add", "buy milk"),
("t-alice", "add", "call mom tonight"),
("t-alice", "add", "fix the bike"),
("t-alice", "add", "learn go"),
("t-bob", "list", ""),
("t-alice", "list", ""),
("t-eve", "list", ""),
]
print(f"{'token':<8} {'do':<5} {'text':<17} {'status':<6} {'reply':<26} job")
for token, action, text in requests:
status, reply, job = handle(token, action, text)
print(f"{token or '(none)':<8} {action:<5} {text:<17} {status:<6} {reply:<26} {job}")import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
static final int FREE_LIMIT = 3; // the free plan keeps at most 3 notes
// The backend's memory: who each token belongs to, and everyone's notes.
static final Map<String, String> TOKENS = Map.of("t-alice", "alice", "t-bob", "bob");
static final Map<String, List<String>> NOTES = new HashMap<>();
record Response(int status, String reply, String job) {}
static int words(String text) {
return text.isBlank() ? 0 : text.trim().split("\\s+").length;
}
// handle is the whole backend: one request in, one response out.
// It returns the status, the reply, and which of the four jobs had the final say.
static Response handle(String token, String action, String text) {
String user = TOKENS.get(token); // protect: who is asking?
if (user == null) return new Response(401, "who are you?", "protect");
List<String> mine = NOTES.computeIfAbsent(user, u -> new ArrayList<>());
switch (action) {
case "add" -> {
if (mine.size() >= FREE_LIMIT) // decide: is this allowed?
return new Response(403, "free plan keeps " + FREE_LIMIT + " notes", "decide");
int count = words(text); // compute: work something out
mine.add(text); // store: remember it for next time
return new Response(201, "saved note " + mine.size() + " (" + count + " words)", "store");
}
case "list" -> {
int total = mine.stream().mapToInt(Main::words).sum(); // compute over what was stored
return new Response(200, mine.size() + " notes, " + total + " words", "compute");
}
default -> {
return new Response(400, "unknown action", "decide");
}
}
}
public static void main(String[] args) {
String[][] requests = {
{"", "add", "buy milk"},
{"t-alice", "add", "buy milk"},
{"t-alice", "add", "call mom tonight"},
{"t-alice", "add", "fix the bike"},
{"t-alice", "add", "learn go"},
{"t-bob", "list", ""},
{"t-alice", "list", ""},
{"t-eve", "list", ""},
};
String row = "%-8s %-5s %-17s %-6s %-26s %s%n";
System.out.printf(row, "token", "do", "text", "status", "reply", "job");
for (String[] r : requests) {
Response res = handle(r[0], r[1], r[2]);
System.out.printf(row, r[0].isEmpty() ? "(none)" : r[0], r[1], r[2], res.status(), res.reply(), res.job());
}
}
}Output (the same in all five languages):
token do text status reply job
(none) add buy milk 401 who are you? protect
t-alice add buy milk 201 saved note 1 (2 words) store
t-alice add call mom tonight 201 saved note 2 (3 words) store
t-alice add fix the bike 201 saved note 3 (3 words) store
t-alice add learn go 403 free plan keeps 3 notes decide
t-bob list 200 0 notes, 0 words compute
t-alice list 200 3 notes, 8 words compute
t-eve list 401 who are you? protect
Reading the output:
- (none) and t-eve: stopped at protect. No known token, so nothing else ran. The backend didn't even look at the text.
- The first three notes from Alice: they pass every door and end in store. Notice the counter,
saved note 1, 2, 3: the backend remembers between requests. The client never sends how many notes Alice has. - "learn go": stopped at decide. Alice is real, and the request is well formed, but the free plan keeps 3 notes. The rule lives here, not in the app.
- Bob's list: 0 notes. The same memory holds Alice's notes, but the backend only ever hands Bob his own. Keeping users apart is part of protect.
- Alice's list: 3 notes, 8 words (2 + 3 + 3). That number was computed from what was stored, by the backend, on demand.
Nothing here touched a network. Lesson 2 put a server around a function like this one; Module 4 shows what happens inside that server when thousands of these calls arrive at once.
Real engineering
- Order the doors by cost. Authentication first, then cheap validation, then rules that need a database read, and only then expensive work. Rejecting early protects the backend from wasted effort and from abuse.
- The four jobs become layers. In real code they usually live in different places: auth in middleware, rules in a service layer, storage in a repository. That separation is what Module 5 is about.
- Store is the hardest part to change. You can rewrite a handler in an afternoon. Changing how millions of saved rows are shaped takes careful migrations. Teams think hardest about the data.
- "Backend" is often many programs. An API server, background workers, a database, a cache and a queue, all working together. Lesson 10 takes one apart.
- Backends are clients too. Payments, email, maps and AI models are other companies' backends. Every call to them can be slow or fail, which is a large part of Module 7.
When it breaks
Each failure below is one of the four jobs missing or in the wrong place.
Pick a failure
GET /notes?user=alice returns Alice's notes to anyone
Think before you read on
The example program keeps notes in a map inside the process. It works perfectly in the output above. What are two things that would go wrong if you put exactly this program on the internet for real users?
Reveal
- Restarts lose everything. The map lives in memory, so every deploy or crash wipes all notes.
- Two copies disagree. Run two copies of the server to handle more traffic, and each has its own map: Alice's note saved by copy A is missing on copy B.
(Also: the tokens are hard-coded and never expire.) Both main problems point to the same fix: store has to move out of the process into a shared database. That's exactly where this module goes next.
Check yourself
1. Name the four jobs of a backend, in the order a request usually meets them.
Answer
Protect (who is asking), decide (is it allowed), compute (work it out), store (remember it).
2. Why should "free plan keeps 3 notes" be checked by the backend even if the app already hides the button?
Answer
Because any client can be old, modified or replaced with curl. Only the backend sees every request, so only the backend can guarantee the rule.
3. Why is it cheaper to check the token before resizing an uploaded photo?
Answer
Checking a token is a quick lookup; resizing a photo costs a lot of CPU and memory. If you resize first, anyone can make your server do expensive work for free.
4. When your backend calls an SMS provider, which side of the client-server line is it on?
Answer
It's the client. For that call, your backend is just another program sending requests to someone else's server, with all the same risks: slow answers, errors, and limits.
Your challenge: add a fifth door
Add a rule to handle: a note may be at most 5 words long. A longer note gets 422 note too long (N words).
Before you write it, decide: does this check belong before or after the plan-limit check? Does it matter which error a user over the limit with a long note sees first?
You've succeeded when sending "t-bob", "add", "one two three four five six" prints status 422 with note too long (6 words), Bob's note count stays at 0, and every other line of the output is unchanged.
The mental model
A backend is the program on your side of the line that every client relies on. For every request it does four jobs, in order of cost: protect (who is it), decide (is it allowed), compute (work it out), store (remember it). The client brings the request; the backend brings the memory and the rules.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →