Part 01 · The Backend Mindset
Why Data Lives in One Place
Why apps keep a single source of truth instead of letting every device hold its own copy.
⏱ 1 hours · Station 7 of 153
Who deleted the eggs?
You and your flatmate share a shopping list app. You're at work and add eggs. At the same moment, your flatmate, at the market, adds milk. That evening the list says: rice, milk. The eggs are gone.
Nobody deleted them. Both phones did exactly what they were built to do: keep a copy of the list, change it, and save it back. The second save simply replaced the first.
Every app that lets more than one device touch the same information runs into this. This lesson is about the simplest and most important answer engineers have found: keep one copy that everyone agrees is the real one, and send it changes, not replacements.
The problem: copies drift apart
When each device keeps its own copy of shared data, three things go wrong.
| Problem | What the user sees |
|---|---|
| Copies disagree | Your phone says 5 items, your laptop says 4. Which is right? |
| Saves overwrite each other | Two people edit at once, one person's change silently disappears |
| Nobody can decide | A rule like "one vote per person" or "don't sell more than 10" needs one place that sees every request. With many copies, no device sees them all |
Lessons 5 and 6 already showed pieces of this: state split between server copies, and two requests racing for one seat. Both have the same root: more than one place thought it held the truth.
Think of the cricket scoreboard
At a cricket match, thousands of people keep track of the score in their heads, on their phones, on paper. They don't always agree: someone missed a wide, someone counted a four twice.
It doesn't matter, because there's one official scorer. When a run is scored, the umpire signals it, the scorer records it, and the big scoreboard shows it. If your count differs, you're wrong; you look at the board and correct yourself.
Notice what the umpire sends to the scorer: not "the score is now 147", but "one run". Changes go to one place; everyone else reads from it.
Where the analogy stops working. A scoreboard is one physical object. A real "single source of truth" is usually a database that may itself run on several machines, with copies for safety and speed (Module 6). "One place" means one logical owner of each piece of data, the one everyone defers to, not literally one disk.
The building blocks
- Source of truth. For each piece of data, exactly one place is the authority. Any other copy (on a phone, in a cache, in a report) is a replica that may be behind, and must be corrected from the source when they differ.
- Centralized data. For most apps, the source of truth for shared data is a database behind the backend. Clients don't own shared data; they ask for it and request changes to it.
- Send changes, not copies. "Add eggs" can be combined with "add milk". "Here is my whole list" can only replace what was there. Operations (add, remove, increment, mark as paid) compose; full snapshots overwrite. This is the same lesson as the atomic update in lesson 6.
- Reads may be stale; writes go to the source. Phones can cache what they last saw, to show something quickly or work offline. But changes must go to the source of truth, which applies them in some order and becomes the new answer.
- The source enforces the rules. Because every change passes through it, the source of truth is the natural place for rules (lesson 3's decide step) and uniqueness (lesson 6). A rule enforced on many copies isn't enforced at all.
Two phones each have a copy of the list [rice]. Phone A adds eggs, phone B adds milk, and each uploads its whole list to the server, A first, then B. What does the server end up with?
Behind the scenes: the lost eggs
Two copies, one list
Step 1/5 Both phones load the list
The server holds [rice]. Each phone downloads it and keeps its own copy, so the app can show it instantly.
- server
- rice
- A copy
- rice
- B copy
- rice
The picture to keep in your head
Picture a star: one source of truth in the middle, every device around the edge.
- Arrows in are changes ("add eggs", "pay order 41"). They're small, they describe intent, and the center decides the order.
- Arrows out are reads. Devices may keep what they read, but they treat it as a snapshot that can be out of date.
- No arrows between devices. Devices never sync with each other directly; if they did, there'd be no single answer.
When you design a feature, ask: where is the one place this data is true? If you can't point to it, two users will eventually disagree.
Try it: where's the truth?
One source of truth, or many copies?
app: 12,400 taka (cached at 10:02)
Code from first principles
The shared shopping list, twice.
- Many copies: each phone keeps its own list and uploads the whole thing, as many simple apps do.
- One source of truth: phones send only the change;
addruns on the server, which owns the list.
package main
import (
"fmt"
"slices"
"strings"
)
func show(step string, a, b, server []string) {
fmt.Printf("%-26s %-18s %-18s %s\n", step, strings.Join(a, ", "), strings.Join(b, ", "), strings.Join(server, ", "))
}
func main() {
fmt.Printf("%-26s %-18s %-18s %s\n", "step", "phone A", "phone B", "server")
// Many copies: each phone keeps its own list and uploads the whole thing.
server := []string{"rice"}
a := slices.Clone(server)
b := slices.Clone(server)
show("start", a, b, server)
a = append(a, "eggs")
show("A adds eggs on the phone", a, b, server)
b = append(b, "milk")
show("B adds milk on the phone", a, b, server)
server = slices.Clone(a) // upload: replace the server's list with mine
show("A uploads its list", a, b, server)
server = slices.Clone(b)
show("B uploads its list", a, b, server)
// One place: phones send the change; only the server holds the list.
fmt.Println("--- one source of truth: send the change, not the copy ---")
server = []string{"rice"}
add := func(item string) { server = append(server, item) } // runs on the server
add("eggs")
show("A sends: add eggs", nil, nil, server)
add("milk")
show("B sends: add milk", nil, nil, server)
show("A and B read the list", server, server, server)
}const show = (step, a, b, server) =>
console.log(`${step.padEnd(26)} ${a.join(", ").padEnd(18)} ${b.join(", ").padEnd(18)} ${server.join(", ")}`);
console.log(`${"step".padEnd(26)} ${"phone A".padEnd(18)} ${"phone B".padEnd(18)} server`);
// Many copies: each phone keeps its own list and uploads the whole thing.
let server = ["rice"];
const a = [...server];
const b = [...server];
show("start", a, b, server);
a.push("eggs");
show("A adds eggs on the phone", a, b, server);
b.push("milk");
show("B adds milk on the phone", a, b, server);
server = [...a]; // upload: replace the server's list with mine
show("A uploads its list", a, b, server);
server = [...b];
show("B uploads its list", a, b, server);
// One place: phones send the change; only the server holds the list.
console.log("--- one source of truth: send the change, not the copy ---");
server = ["rice"];
const add = (item) => server.push(item); // runs on the server
add("eggs");
show("A sends: add eggs", [], [], server);
add("milk");
show("B sends: add milk", [], [], server);
show("A and B read the list", server, server, server);fn show(step: &str, a: &[&str], b: &[&str], server: &[&str]) {
println!("{:<26} {:<18} {:<18} {}", step, a.join(", "), b.join(", "), server.join(", "));
}
// Runs on the server: the only place the list is changed.
fn add<'a>(server: &mut Vec<&'a str>, item: &'a str) {
server.push(item);
}
fn main() {
println!("{:<26} {:<18} {:<18} server", "step", "phone A", "phone B");
// Many copies: each phone keeps its own list and uploads the whole thing.
let mut server = vec!["rice"];
let mut a = server.clone();
let mut b = server.clone();
show("start", &a, &b, &server);
a.push("eggs");
show("A adds eggs on the phone", &a, &b, &server);
b.push("milk");
show("B adds milk on the phone", &a, &b, &server);
server = a.clone(); // upload: replace the server's list with mine
show("A uploads its list", &a, &b, &server);
server = b.clone();
show("B uploads its list", &a, &b, &server);
// One place: phones send the change; only the server holds the list.
println!("--- one source of truth: send the change, not the copy ---");
let mut server = vec!["rice"];
add(&mut server, "eggs");
show("A sends: add eggs", &[], &[], &server);
add(&mut server, "milk");
show("B sends: add milk", &[], &[], &server);
show("A and B read the list", &server, &server, &server);
}def show(step, a, b, server):
print(f"{step:<26} {', '.join(a):<18} {', '.join(b):<18} {', '.join(server)}")
print(f"{'step':<26} {'phone A':<18} {'phone B':<18} server")
# Many copies: each phone keeps its own list and uploads the whole thing.
server = ["rice"]
a = list(server)
b = list(server)
show("start", a, b, server)
a.append("eggs")
show("A adds eggs on the phone", a, b, server)
b.append("milk")
show("B adds milk on the phone", a, b, server)
server = list(a) # upload: replace the server's list with mine
show("A uploads its list", a, b, server)
server = list(b)
show("B uploads its list", a, b, server)
# One place: phones send the change; only the server holds the list.
print("--- one source of truth: send the change, not the copy ---")
server = ["rice"]
def add(item): # runs on the server
server.append(item)
add("eggs")
show("A sends: add eggs", [], [], server)
add("milk")
show("B sends: add milk", [], [], server)
show("A and B read the list", server, server, server)import java.util.ArrayList;
import java.util.List;
public class Main {
static void show(String step, List<String> a, List<String> b, List<String> server) {
System.out.printf("%-26s %-18s %-18s %s%n", step, String.join(", ", a), String.join(", ", b), String.join(", ", server));
}
// Runs on the server: the only place the list is changed.
static void add(List<String> server, String item) {
server.add(item);
}
public static void main(String[] args) {
System.out.printf("%-26s %-18s %-18s %s%n", "step", "phone A", "phone B", "server");
// Many copies: each phone keeps its own list and uploads the whole thing.
List<String> server = new ArrayList<>(List.of("rice"));
List<String> a = new ArrayList<>(server);
List<String> b = new ArrayList<>(server);
show("start", a, b, server);
a.add("eggs");
show("A adds eggs on the phone", a, b, server);
b.add("milk");
show("B adds milk on the phone", a, b, server);
server = new ArrayList<>(a); // upload: replace the server's list with mine
show("A uploads its list", a, b, server);
server = new ArrayList<>(b);
show("B uploads its list", a, b, server);
// One place: phones send the change; only the server holds the list.
System.out.println("--- one source of truth: send the change, not the copy ---");
server = new ArrayList<>(List.of("rice"));
add(server, "eggs");
show("A sends: add eggs", List.of(), List.of(), server);
add(server, "milk");
show("B sends: add milk", List.of(), List.of(), server);
show("A and B read the list", server, server, server);
}
}Output (the same in all five languages):
step phone A phone B server
start rice rice rice
A adds eggs on the phone rice, eggs rice rice
B adds milk on the phone rice, eggs rice, milk rice
A uploads its list rice, eggs rice, milk rice, eggs
B uploads its list rice, eggs rice, milk rice, milk
--- one source of truth: send the change, not the copy ---
A sends: add eggs rice, eggs
B sends: add milk rice, eggs, milk
A and B read the list rice, eggs, milk rice, eggs, milk rice, eggs, milk
Reading the output:
- A adds eggs / B adds milk: each phone's copy changes, the server's doesn't. Both users see a list that looks right to them.
- A uploads, then B uploads: the server's list goes to
rice, eggs, then torice, milk. B's upload didn't "remove" eggs; it replaced the list with a copy that never had them. - One source of truth: the server applies
add eggsand thenadd milkto the same list, so both survive:rice, eggs, milk. - A and B read the list: both phones now show the same thing, because they both read from the one place it's true.
The only change between the two halves is what the phone sends: its whole copy, or what the user did.
Real engineering
- The database is the usual source of truth. Backends are stateless (lesson 5) so they can scale; the shared database holds the truth and enforces constraints. (Module 6.)
- Design APIs around changes.
POST /lists/7/items {name: eggs}andDELETE /lists/7/items/31compose;PUT /lists/7 [whole list]overwrites. When you must accept whole documents, use version numbers so a stale copy is rejected instead of saved (optimistic concurrency, lesson 6). - Every other copy is a cache. Mobile app storage, CDN, Redis, search indexes and analytics tables are replicas. Each needs a rule for how stale it may be and how it gets refreshed. (Module 6, lessons 17–18.)
- Offline-first apps still have a source. Apps that work offline queue the user's changes and send them when they reconnect; the server merges them in order. Truly peer-to-peer syncing uses special data types (CRDTs), which is an advanced topic.
- One logical source can be many machines. Databases replicate for safety and read speed (Module 6, lesson 15), and very large ones split data across machines (lesson 16). Each piece of data still has exactly one authoritative home.
When it breaks
Pick a failure
PUT /profile {name, bio, photo}
bio edited on laptop is reverted by phoneThink before you read on
If keeping one source of truth is so important, why do almost all mobile apps keep a local copy of the data anyway?
Reveal
Speed and offline use. Reading from the phone is instant, and works on a bus with no signal. The trick is that the local copy is treated as a cache: it shows what the user last knew, and every change the user makes is sent to the server as an operation, which is the one place that decides. When the server's answer comes back, the local copy is corrected. Local copies are fine; local authority is the problem.
Check yourself
1. What does "single source of truth" mean?
Answer
For each piece of data, one place is the authority. Every other copy is a replica that may be stale and is corrected from that source.
2. Why does "add milk" survive alongside "add eggs", while "here is my list" doesn't?
Answer
Operations can be applied one after another to the current data, so both take effect. A full copy can only replace what's there, so the last upload wins and earlier changes are lost.
3. Why should a rule like "at most 10 per customer" be enforced at the source of truth?
Answer
Because only the source sees every change from every device. Checking on one copy can't account for changes made on another.
4. Name two kinds of replica that a backend commonly uses.
Answer
For example: a cache (Redis, an in-memory copy), a CDN, a read replica of the database, a search index, or the data stored in a mobile app.
Your challenge: remove without losing
Extend the one-source-of-truth half of the program with a remove operation. Then play this scenario:
- The list is
rice, eggs, milk. - Phone A removes
eggs. At the same time, phone B, which still shows the old list, changeseggsto12 eggsby sendingremove eggsandadd 12 eggs.
Before you run it, predict the final list. Is it what both users wanted? What would you have to send instead of "remove by name" to avoid surprising either user?
You've succeeded when your program prints the final list, and you can explain why giving each item an id and sending remove item 31 or rename item 31 to "12 eggs" makes the outcome easier to reason about than removing by name.
The mental model
Shared data needs one place where it's true. Devices and servers can keep copies to be fast, but those copies are caches that may be stale. Changes are sent to the source as operations ("add eggs"), not as replacement copies ("my list is..."), so they combine instead of overwriting. The source of truth orders the changes, enforces the rules and answers every read that matters.
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 →