Peer-to-peer
Peer-to-peer systems have no centre: every participant both asks and serves, and information spreads by peers talking to peers. This page lets Pedal's docks keep working when their district loses its link to the servers, gossiping bike counts until every dock agrees, and shows why gossip reaches thousands of peers in only a few more rounds than it takes for dozens.
1. In one picture
In a peer-to-peer architecture every participant, a peer, is both a client and a server: it asks for things and supplies them. There's no central server that everything depends on. Peers find each other through a few known starting points or a distributed index, and spread information by talking directly, often by gossip (each peer periodically swaps what it knows with a few others).
Well-known uses: BitTorrent file sharing, blockchains, the gossip that keeps Cassandra and Consul clusters aware of each other, and local-first apps that sync phones directly. In Pedal it's a fallback: when a district's docks lose their link to Pedal's servers, they can still share which of them have bikes.
2. Pedal built this way
The district's link to Pedal is down. D1 and D2 are empty; riders at D1 need to know where bikes are. Each dock knows only its own count at first.
Versions make the merge safe: when two peers disagree about D2, the higher version wins, so old news never overwrites new news. Peers converge on the same picture without any coordinator; until they do, different docks may give different answers (eventual consistency, System design 16).
3. The rules
| Rule | Why it exists | Broken when… |
|---|---|---|
| No peer is special | Any peer can leave or fail without stopping the rest. | One "main" dock that every other dock must reach. |
| State spreads by peers talking to peers | Information reaches everyone without a central copy. | Peers poll a server for the full list. |
| Merges are deterministic | Every peer reaches the same answer from the same facts. | Two peers keep different values for D2 forever because "last received" wins. |
| Peers verify what they're told | A misbehaving or malicious peer can't poison the network. | A dock trusts any message claiming 50 bikes somewhere. |
4. How it goes wrong
5. The code
Six docks in a ring (with one shortcut) each holding versioned bike counts. Each round, every dock swaps views with one neighbour and keeps the newer version of every entry. After three rounds all six agree, and D1 can send riders to D3.
from __future__ import annotations
class Dock:
"""A peer: holds its own status plus what it has heard about others, as (version, bikes)."""
def __init__(self, name: str, bikes: int) -> None:
self.name = name
self.view: dict[str, tuple[int, int]] = {name: (1, bikes)}
self.neighbours: list["Dock"] = []
def update_self(self, bikes: int) -> None:
version, _ = self.view[self.name]
self.view[self.name] = (version + 1, bikes)
def gossip_with(self, other: "Dock") -> None:
# push-pull: both sides keep the newer version of every entry
for a, b in ((self, other), (other, self)):
for dock, (ver, bikes) in list(a.view.items()):
if dock not in b.view or b.view[dock][0] < ver:
b.view[dock] = (ver, bikes)
def nearest_with_bikes(self) -> str:
options = sorted((d for d, (_, n) in self.view.items() if n > 0 and d != self.name))
return options[0] if options else "none known"
if __name__ == "__main__":
docks = [Dock(f"D{i}", b) for i, b in enumerate([0, 3, 5, 2, 0, 4], start=1)]
for i, d in enumerate(docks): # a ring, plus one shortcut
d.neighbours = [docks[(i + 1) % 6], docks[(i - 1) % 6]]
docks[0].neighbours.append(docks[3])
print("the link to Pedal's servers is down; docks gossip with neighbours instead")
docks[1].update_self(0) # D2 empties: version 2 says 0 bikes
rounds = 0
while not all(len(d.view) == 6 and d.view["D2"] == (2, 0) for d in docks):
rounds += 1
for i, d in enumerate(docks):
d.gossip_with(d.neighbours[(rounds + i) % len(d.neighbours)])
print(f"round {rounds}: docks knowing all 6 = {sum(len(d.view) == 6 for d in docks)}")
print("D1 has no bikes; it sends riders to", docks[0].nearest_with_bikes(), "| D1 knows D2 =", docks[0].view["D2"])
// A peer: holds its own status plus what it has heard about others, as [version, bikes].
class Dock {
view = new Map<string, [number, number]>();
neighbours: Dock[] = [];
constructor(readonly name: string, bikes: number) { this.view.set(name, [1, bikes]); }
updateSelf(bikes: number): void { const [v] = this.view.get(this.name) as [number, number]; this.view.set(this.name, [v + 1, bikes]); }
gossipWith(other: Dock): void {
// push-pull: both sides keep the newer version of every entry
for (const [a, b] of [[this, other], [other, this]] as [Dock, Dock][]) {
for (const [dock, [ver, bikes]] of [...a.view]) {
const known = b.view.get(dock);
if (!known || known[0] < ver) b.view.set(dock, [ver, bikes]);
}
}
}
nearestWithBikes(): string {
const options = [...this.view].filter(([d, [, n]]) => n > 0 && d !== this.name).map(([d]) => d).sort();
return options.length ? options[0] : "none known";
}
}
const docks = [0, 3, 5, 2, 0, 4].map((b, i) => new Dock(`D${i + 1}`, b));
docks.forEach((d, i) => { d.neighbours = [docks[(i + 1) % 6], docks[(i + 5) % 6]]; }); // a ring, plus one shortcut
docks[0].neighbours.push(docks[3]);
console.log("the link to Pedal's servers is down; docks gossip with neighbours instead");
docks[1].updateSelf(0); // D2 empties: version 2 says 0 bikes
const done = (): boolean => docks.every((d) => d.view.size === 6 && (d.view.get("D2") as [number, number])[0] === 2);
let rounds = 0;
while (!done()) {
rounds++;
docks.forEach((d, i) => d.gossipWith(d.neighbours[(rounds + i) % d.neighbours.length]));
console.log(`round ${rounds}: docks knowing all 6 = ${docks.filter((d) => d.view.size === 6).length}`);
}
const d2 = docks[0].view.get("D2") as [number, number];
console.log(`D1 has no bikes; it sends riders to ${docks[0].nearestWithBikes()} | D1 knows D2 = (${d2[0]}, ${d2[1]})`);import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// A peer: holds its own status plus what it has heard about others, as {version, bikes}.
class Dock {
final String name;
final Map<String, int[]> view = new HashMap<>();
List<Dock> neighbours = new ArrayList<>();
Dock(String name, int bikes) { this.name = name; view.put(name, new int[]{1, bikes}); }
void updateSelf(int bikes) { int v = view.get(name)[0]; view.put(name, new int[]{v + 1, bikes}); }
void gossipWith(Dock other) {
// push-pull: both sides keep the newer version of every entry
Dock[][] pairs = {{this, other}, {other, this}};
for (Dock[] p : pairs) {
for (Map.Entry<String, int[]> e : new ArrayList<>(p[0].view.entrySet())) {
int[] known = p[1].view.get(e.getKey());
if (known == null || known[0] < e.getValue()[0]) p[1].view.put(e.getKey(), e.getValue().clone());
}
}
}
String nearestWithBikes() {
return view.entrySet().stream().filter(e -> e.getValue()[1] > 0 && !e.getKey().equals(name)).map(Map.Entry::getKey).sorted().findFirst().orElse("none known");
}
}
public class Main {
public static void main(String[] args) {
int[] bikes = {0, 3, 5, 2, 0, 4};
List<Dock> docks = new ArrayList<>();
for (int i = 0; i < 6; i++) docks.add(new Dock("D" + (i + 1), bikes[i]));
for (int i = 0; i < 6; i++) docks.get(i).neighbours = new ArrayList<>(List.of(docks.get((i + 1) % 6), docks.get((i + 5) % 6))); // a ring, plus one shortcut
docks.get(0).neighbours.add(docks.get(3));
System.out.println("the link to Pedal's servers is down; docks gossip with neighbours instead");
docks.get(1).updateSelf(0); // D2 empties: version 2 says 0 bikes
int rounds = 0;
while (!docks.stream().allMatch(d -> d.view.size() == 6 && d.view.get("D2")[0] == 2)) {
rounds++;
for (int i = 0; i < 6; i++) { Dock d = docks.get(i); d.gossipWith(d.neighbours.get((rounds + i) % d.neighbours.size())); }
System.out.println("round " + rounds + ": docks knowing all 6 = " + docks.stream().filter(d -> d.view.size() == 6).count());
}
int[] d2 = docks.get(0).view.get("D2");
System.out.println("D1 has no bikes; it sends riders to " + docks.get(0).nearestWithBikes() + " | D1 knows D2 = (" + d2[0] + ", " + d2[1] + ")");
}
}Output (the same in all three languages)
the link to Pedal's servers is down; docks gossip with neighbours instead round 1: docks knowing all 6 = 0 round 2: docks knowing all 6 = 4 round 3: docks knowing all 6 = 6 D1 has no bikes; it sends riders to D3 | D1 knows D2 = (2, 0)
6. Try it
How fast does gossip spread?
One peer learns something new. Every round, each peer that knows it tells a few random others. Pick the network size and how many each peer tells.
Simulated with a fixed random seed. Rounds grow with the logarithm of the number of peers: with fan-out 1, ten times more peers adds about five or six rounds.
7. What it costs, and when to pick another style
What you get
- No single point of failure: the network survives any peer leaving.
- Scales with participants: every new peer adds capacity as well as demand.
- Works without a centre: offline districts, censorship-resistant or trustless settings.
- Cheap distribution: peers share the load of serving data.
What you pay
- Eventual agreement only: peers disagree until gossip converges.
- Security is hard: any peer may lie; trust must be designed in.
- Hard to observe and debug: no central log of what happened.
- Unreliable peers: devices come and go, with slow links.
| If… | Consider instead |
|---|---|
| A trusted, reliable centre exists | Client-server |
| You need strict, immediate consistency | A central database or a consensus group |
| Peers need routing to the right server, not equality | Broker |
| Many identical copies of a central system | Cell-based |
8. Signs you've outgrown it
- Most traffic ends up going through a few powerful peers: a central server in disguise.
- Conflicts and security incidents take most of the team's time.
- Users expect everyone to see the same answer immediately.
9. Remember it and say it
How this hook works: what “Village news over the fence” means
What the slogan means, phrase by phrase
- “Village news”
- Information with no central publisher.
- “over the fence”
- Each peer tells a few neighbours, who tell theirs.
Why this picture fits: News in a village without a newspaper reaches everyone in a few rounds of neighbours talking.
The story → the concept
| The village | Pedal |
|---|---|
| Villagers | Docks in a district |
| Gossip over the fence | Docks swapping availability with neighbours |
| "That's old news" | Higher versions win |
| Someone on holiday | A dock that's offline; the news goes round it |
Watch it on an example
- After three rounds every dock knows every count.
- D2's newer "0 bikes" beats its older "3 bikes" everywhere.
- D1 sends riders to D3 without asking any server.
The trap it saves you from
Building peer-to-peer where a reliable central server would do, and paying its costs for nothing.
It still works when…
Inside other systems: clusters like Cassandra and Consul gossip membership peer-to-peer while serving clients client-server.
In an interview or design review
- Say why there's no centre: offline operation, no trusted party, or load spread across participants.
- Describe how state spreads and merges: gossip with fan-out, versions or vector clocks, convergence in about log N rounds.
- Address trust: authenticated peers in closed networks; signatures, consensus or reputation in open ones.
10. Check yourself
What's the defining property of a peer?
Two docks disagree about D2's bike count. How do peers settle it?
With fan-out 1, all 100 peers know after about 12 rounds. Roughly how many rounds for 10,000 peers?
When is client-server the better choice?
11. Go deeper
| Resource | Why |
|---|---|
| System design 14: Replication | Gossip, anti-entropy and leaderless replication in clusters. |
| System design 20: Time and ordering | Versions and vector clocks for merging. |
| The BitTorrent protocol specification | A widely used peer-to-peer system, in its own words. |