Coding by Hand
Rust home

Design LinkedIn

A high school yearbook is a stack of profiles bound together, and the back of each portrait carries the signatures of the people who knew that person well enough to write a line. LinkedIn is the same stack, except the signatures arrive after graduation and they pile up year after year. A profile holds the name and the headline. A signature on the back is an endorsement for a skill. The handshake that lets you sign at all is the connection — and a connection only counts after both people agree to it. Build the yearbook and the rest of the product is signatures on the back of the page.

A small social graph showing three profiles connected as nodes with labeled edges
A small social graph showing three profiles connected as nodes with labeled edges

Reid Hoffman shipped the first version of LinkedIn out of his living room in 2003, two years before Facebook reached the public, and the bottleneck he was solving was different from the one a social network solves. The problem he picked was the cold introduction. Before LinkedIn, the way a recruiter reached an engineer at another company was a phone call to a friend who knew a friend who might know the right person, and 4 out of 5 of those chains broke before the message arrived. Hoffman's bet was that the chain would survive if every link in it was stored as a fact on a server — a real connection one human had accepted from another. The accepted-connection rule is the heart of the product. A request that one side never confirms is just an email. A request both sides confirm is a directed graph edge the recruiter can walk.

Start with the smallest types the network needs.

#[derive(Clone)]
struct Profile {
    name: &'static str,
    headline: &'static str,
    skills: Vec<&'static str>,
}

#[derive(Clone, Copy)]
enum Event {
    SendRequest { from: &'static str, to: &'static str },
    AcceptRequest { from: &'static str, to: &'static str },
    Endorse { endorser: &'static str, endorsee: &'static str, skill: &'static str },
    ViewProfile { user: &'static str },
}

struct Network {
    profiles: BTreeMap<&'static str, Profile>,
    pending: BTreeMap<&'static str, BTreeSet<&'static str>>,
    connections: BTreeMap<&'static str, BTreeSet<&'static str>>,
    endorsements: BTreeMap<(&'static str, &'static str), Vec<&'static str>>,
}

A Profile is a name, a one-line headline, and a list of skills the person claims. The skills field is a Vec<&'static str> because the lesson hardcodes the data — in a real product it would be owned Strings pulled from a database. The Event enum is the list of doors the outside world can knock on. SendRequest goes in one direction — the sender knocks on the receiver's pending list. AcceptRequest flips the pending edge into a real two-way connection. Endorse adds a name to the back of a specific skill on a specific profile. ViewProfile is the read path that prints what everyone else has been writing.

The Network struct holds 4 maps and every one of them is a BTreeMap or a BTreeSet on purpose. A HashMap would scramble the iteration order from one run to the next, which would mean the program prints a different ordering of profiles every time it runs, which would mean the snapshot test that pins the output would fail on someone else's machine. BTreeMap walks its keys in sorted order, so the output is byte-identical no matter where the binary runs. The endorsements map is keyed on a (profile, skill) tuple because a skill only exists in the context of the person who claims it — Ben's "rust" endorsements are a different list from Ada's "rust" endorsements, and the tuple key makes both lists addressable by one lookup.

Two-step handshake turning a directed request into an undirected connection
Two-step handshake turning a directed request into an undirected connection

The handle method is the door table. Every event runs through the same match and every arm returns a string the driver can print.

impl Network {
    fn new() -> Self {
        Self {
            profiles: BTreeMap::new(),
            pending: BTreeMap::new(),
            connections: BTreeMap::new(),
            endorsements: BTreeMap::new(),
        }
    }

    fn add_profile(&mut self, profile: Profile) {
        self.profiles.insert(profile.name, profile);
    }

    fn handle(&mut self, event: Event) -> String {
        match event {
            Event::SendRequest { from, to } => {
                if from == to {
                    return format!("err: {from} cannot connect to self");
                }
                if !self.profiles.contains_key(from) || !self.profiles.contains_key(to) {
                    return "err: unknown profile".into();
                }
                let already = self
                    .connections
                    .get(from)
                    .is_some_and(|set| set.contains(to));
                if already {
                    return format!("err: {from} and {to} already connected");
                }
                self.pending.entry(to).or_default().insert(from);
                format!("request: {from} -> {to} (pending)")
            }
            Event::AcceptRequest { from, to } => {
                let exists = self
                    .pending
                    .get(to)
                    .is_some_and(|set| set.contains(from));
                if !exists {
                    return format!("err: no pending request {from} -> {to}");
                }
                self.pending.get_mut(to).unwrap().remove(from);
                self.connections.entry(from).or_default().insert(to);
                self.connections.entry(to).or_default().insert(from);
                format!("accept: {from} <-> {to} (connected)")
            }
            Event::Endorse { endorser, endorsee, skill } => {
                let connected = self
                    .connections
                    .get(endorser)
                    .is_some_and(|set| set.contains(endorsee));
                if !connected {
                    return format!("err: {endorser} must connect to {endorsee} first");
                }
                let has_skill = self
                    .profiles
                    .get(endorsee)
                    .is_some_and(|p| p.skills.contains(&skill));
                if !has_skill {
                    return format!("err: {endorsee} does not list {skill}");
                }
                let list = self.endorsements.entry((endorsee, skill)).or_default();
                if list.contains(&endorser) {
                    return format!("err: {endorser} already endorsed {endorsee} for {skill}");
                }
                list.push(endorser);
                format!("endorse: {endorser} -> {endorsee} for {skill}")
            }
            Event::ViewProfile { user } => format!("view: {user}"),
        }
    }
}

Read the SendRequest arm first. It refuses self-connections, it refuses unknown profiles, and it refuses to send a second request once two people are already connected. The check uses is_some_and on the optional set lookup — the cheapest way to ask "does this map have a set, and does that set contain this name" without unwrapping or allocating. If every guard passes, the receiver's pending set grows by one entry, and the message comes back tagged pending. Nothing in the connections map moved. A request is a knock on the door, not an open door.

AcceptRequest is the second half of the handshake. It checks that the pending edge actually exists, removes it from the pending set, and then writes the connection into both directions of the connections map. The two writes are what makes the connection undirected. The graph that recruiters search later does not care who sent the request — they walk Ada to Ben and Ben to Ada through the same edge. If the writes were one-sided, a search starting from Ben would not find Ada through this edge, and the product would silently lose half the chain.

Endorse adds the endorser's name to the vector at (endorsee, skill). It guards 3 ways. The endorser must already be connected to the endorsee, because endorsing a stranger is a spam signal and the product banned it in 2014 after exactly this kind of abuse. The endorsee must actually list the skill, because a one-click endorsement for a skill the person never claimed is meaningless. And the same endorser cannot stack 2 endorsements for the same person on the same skill, because the count is the number of distinct humans who vouched, not the number of clicks.

The render side is the read path, and it is one method long.

impl Network {
    fn render_profile(&self, name: &str) -> String {
        let profile = match self.profiles.get(name) {
            Some(p) => p,
            None => return format!("  (no profile for {name})\n"),
        };
        let empty = BTreeSet::new();
        let connections = self.connections.get(name).unwrap_or(&empty);
        let mut out = String::new();
        out.push_str(&format!("  name: {}\n", profile.name));
        out.push_str(&format!("  headline: {}\n", profile.headline));
        out.push_str("  connections: [");
        for (i, c) in connections.iter().enumerate() {
            if i > 0 {
                out.push_str(", ");
            }
            out.push_str(c);
        }
        out.push_str("]\n");
        out.push_str("  skills:\n");
        let mut skills: Vec<&&str> = profile.skills.iter().collect();
        skills.sort();
        for skill in skills {
            let key = (profile.name, *skill);
            let endorsers = self
                .endorsements
                .get(&key)
                .map(|v| v.as_slice())
                .unwrap_or(&[]);
            let mut sorted: Vec<&&str> = endorsers.iter().collect();
            sorted.sort();
            out.push_str(&format!("    - {} ({} endorsements)", skill, sorted.len()));
            if !sorted.is_empty() {
                out.push_str(": ");
                for (i, e) in sorted.iter().enumerate() {
                    if i > 0 {
                        out.push_str(", ");
                    }
                    out.push_str(e);
                }
            }
            out.push('\n');
        }
        out
    }
}

render_profile builds a string instead of printing because the same renderer feeds both the view event and the final dump of all profiles. The connections list iterates a BTreeSet, which means the names always appear in the same order — [ben, cara], not [cara, ben] on every other run. The skills loop sorts the profile's own skill list, then looks up the endorsement vector for each (name, skill) tuple, then sorts the endorsers before printing them. Three sort calls for three different things — the skills on the profile, the endorsers on each skill, the connections in the set — all so the output is the same on every machine.

Endorsement list for one profile showing skill name and endorser count
Endorsement list for one profile showing skill name and endorser count

The driver hardcodes 3 profiles and 8 events. The events are a small but honest slice of the product — Ada and Ben handshake, Cara and Ada handshake, three endorsements land, and a view event prints Ada's page mid-stream.

fn main() {
    let mut net = Network::new();
    net.add_profile(Profile {
        name: "ada",
        headline: "founder at gear works",
        skills: vec!["rust", "systems", "leadership"],
    });
    net.add_profile(Profile {
        name: "ben",
        headline: "staff engineer at gridscale",
        skills: vec!["rust", "distributed-systems"],
    });
    net.add_profile(Profile {
        name: "cara",
        headline: "designer at northbeam",
        skills: vec!["product", "leadership"],
    });

    let events = [
        Event::SendRequest { from: "ada", to: "ben" },
        Event::AcceptRequest { from: "ada", to: "ben" },
        Event::SendRequest { from: "cara", to: "ada" },
        Event::AcceptRequest { from: "cara", to: "ada" },
        Event::Endorse { endorser: "ben", endorsee: "ada", skill: "rust" },
        Event::Endorse { endorser: "cara", endorsee: "ada", skill: "leadership" },
        Event::Endorse { endorser: "ada", endorsee: "ben", skill: "rust" },
        Event::ViewProfile { user: "ada" },
    ];

    println!("--- event log ---");
    for event in events {
        let view = matches!(event, Event::ViewProfile { .. });
        let user = if let Event::ViewProfile { user } = event {
            Some(user)
        } else {
            None
        };
        println!("{}", net.handle(event));
        if view {
            if let Some(u) = user {
                print!("{}", net.render_profile(u));
            }
        }
    }

    println!();
    println!("--- all profiles ---");
    let names: Vec<&&str> = net.profiles.keys().collect();
    for name in names {
        print!("{}", net.render_profile(name));
    }
}
--- event log ---
request: ada -> ben (pending)
accept: ada <-> ben (connected)
request: cara -> ada (pending)
accept: cara <-> ada (connected)
endorse: ben -> ada for rust
endorse: cara -> ada for leadership
endorse: ada -> ben for rust
view: ada
  name: ada
  headline: founder at gear works
  connections: [ben, cara]
  skills:
    - leadership (1 endorsements): cara
    - rust (1 endorsements): ben
    - systems (0 endorsements)

--- all profiles ---
  name: ada
  headline: founder at gear works
  connections: [ben, cara]
  skills:
    - leadership (1 endorsements): cara
    - rust (1 endorsements): ben
    - systems (0 endorsements)
  name: ben
  headline: staff engineer at gridscale
  connections: [ada]
  skills:
    - distributed-systems (0 endorsements)
    - rust (1 endorsements): ada
  name: cara
  headline: designer at northbeam
  connections: [ada]
  skills:
    - leadership (0 endorsements)
    - product (0 endorsements)

Read the event log from the top. The first 4 lines are the two handshakes — a request and then an accept, in both directions. The next 3 are the endorsements. Ben endorses Ada for rust because they are connected and Ada lists rust. Cara endorses Ada for leadership because they are connected and Ada lists leadership. Ada endorses Ben for rust because Ada and Ben are connected and Ben lists rust. Then the view event prints Ada's full page — name, headline, the two connections, and the 3 skills with their endorsement counts. Rust shows 1 endorsement from Ben. Leadership shows 1 endorsement from Cara. Systems shows 0, because nobody endorsed it. The dump at the bottom walks all 3 profiles in alphabetical order and shows the same shape for each.

One question worth asking — why does the skill ordering in Ada's profile come out as leadership, rust, systems when the source code lists them as rust, systems, leadership? Look at the render code. The skill loop sorts the list before printing. The source order is the order Ada claimed them on her profile, but the display order is alphabetical so the reader can scan the page without hunting. The product does this in real life for the same reason — a long skill list that prints in claim-order looks random, and a sorted list looks like a directory.

The thing this design cannot do is answer "how do I reach Cara through my network?" The graph stores who knows whom, but it does not yet walk the graph to find the shortest chain of friends-of-friends from one person to another. The next bottleneck is the breadth-first search that turns the connection map into a degree-of-separation query — which is what every "you are 2 connections away" line on LinkedIn actually runs underneath.