InterviewKata
Architecture styles · 14 of 16 · Scale and decentralisation

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

News in a village with no newspaper: each person tells a couple of neighbours over the fence, and they tell theirs. Nobody is in charge, nobody has the complete picture at first, and yet within a few rounds everyone knows. If one person is away, the news goes round them.

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

RuleWhy it existsBroken when…
No peer is specialAny peer can leave or fail without stopping the rest.One "main" dock that every other dock must reach.
State spreads by peers talking to peersInformation reaches everyone without a central copy.Peers poll a server for the full list.
Merges are deterministicEvery 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 toldA misbehaving or malicious peer can't poison the network.A dock trusts any message claiming 50 bikes somewhere.

4. How it goes wrong

No versions or clocksOld news arrives late and overwrites new news; use version numbers or vector clocks.
Trusting every peerOpen networks need signatures, reputation or consensus; closed ones still need authentication.
Expecting instant agreementGossip converges in rounds; answers differ until it has.
Using it where a centre is fineIf a reliable central service exists, client-server is simpler to build, secure and debug.

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.

Peers
Peers told per round (fan-out)

Share of peers that know, round by round. Green: everyone.

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 existsClient-server
You need strict, immediate consistencyA central database or a consensus group
Peers need routing to the right server, not equalityBroker
Many identical copies of a central systemCell-based

8. Signs you've outgrown it

9. Remember it and say it

Memory hook: "Village news over the fence"Every peer is both client and server; information spreads peer to peer until everyone agrees.
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 villagePedal
VillagersDocks in a district
Gossip over the fenceDocks swapping availability with neighbours
"That's old news"Higher versions win
Someone on holidayA dock that's offline; the news goes round it

Watch it on an example

  1. After three rounds every dock knows every count.
  2. D2's newer "0 bikes" beats its older "3 bikes" everywhere.
  3. 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

10. Check yourself

What's the defining property of a peer?

There's no client-server split.

Two docks disagree about D2's bike count. How do peers settle it?

A deterministic merge rule makes all peers converge.

With fan-out 1, all 100 peers know after about 12 rounds. Roughly how many rounds for 10,000 peers?

Spread is exponential, so rounds grow with the logarithm of the number of peers: 100 times more peers roughly doubles the rounds here.

When is client-server the better choice?

Peer-to-peer pays real costs for having no centre.

11. Go deeper

ResourceWhy
System design 14: ReplicationGossip, anti-entropy and leaderless replication in clusters.
System design 20: Time and orderingVersions and vector clocks for merging.
The BitTorrent protocol specificationA widely used peer-to-peer system, in its own words.