You start by writing small Unix tools (a Xargs, a Find, a ping-pong pipe program) directly against xv6's syscall interface, then add two new syscalls yourself — a syscall tracer and a sysinfo call that reports kernel state. This is the on-ramp: it forces you to read the xv6 source and understand exactly how a process asks the kernel to do something, before you touch anything that runs in kernel mode.
You print a process's page table, then add a second, larger address space per process to speed up syscalls, before moving into how xv6 handles traps — the mechanism that switches from user mode to kernel mode on a syscall, interrupt, or exception. By the end you understand exactly what "the CPU jumps into the kernel" means at the level of RISC-V registers, the trampoline page, and the trapframe.
fork() in xv6 naively copies the entire parent address space — you rewrite it to share physical pages between parent and child and only copy-on-write when either one writes, with a reference count to avoid freeing memory too early. Then you go multiprocessor: replace xv6's coarse kernel lock with fine-grained locks in the memory allocator and the buffer cache, and watch throughput actually scale with cores instead of serializing on one giant lock.
You extend xv6's on-disk file system to support larger files and symbolic links, then implement mmap() to map files directly into a process's address space with lazy, page-fault-driven loading. The last lab hands you a real NIC (the E1000) and has you write the driver's transmit and receive paths — so the semester ends with your kernel talking to the outside world, not just to itself.
Checkpoint 0 has you write a tiny web crawler against the kernel's own TCP socket so you've felt what the abstraction you're about to build actually does, then implement an in-memory reliable byte stream. Checkpoint 1 is the Reassembler: network segments arrive out of order and can overlap or duplicate, and your job is to stitch arbitrary incoming byte ranges back into one ordered stream — the unglamorous piece that makes everything after it possible.
You implement the receiving half of TCP: turning incoming segments into calls on your Reassembler, tracking the 32-bit sequence-number space (with wraparound and the initial-sequence-number offset), and reporting a correct window size back to the sender so it knows how much more it's allowed to send. This is where TCP's "reliable stream over an unreliable network" promise actually gets kept — and where the classic off-by-one sequence number bugs live.
The sender side has to decide when to send, what to send, and when to give up waiting and retransmit — implemented with a retransmission timer that doubles on every consecutive timeout (exponential backoff) and resets once an ACK makes progress. Put your receiver and sender together and you have a working, RFC-compliant TCP that can hold a real connection with an actual server on the internet — the moment this course is built around.
The last checkpoints drop down a layer: a NetworkInterface that translates between IP datagrams and Ethernet frames using ARP (with a cache and retransmission of pending requests), and then an IP router that holds a forwarding table and picks the correct outgoing interface by longest-prefix match. Finish this and you've personally implemented every layer between "a program calls send()" and "a frame goes out on the wire."
Project 0 is a copy-on-write trie built with a strict set of C++ rules — no raw pointers, no locks, only shared_ptr and immutability — that must be completed with a perfect score before you're allowed to touch BusTub itself, the teaching relational database the CMU Database Group built specifically for this course. It exists purely to filter out "I don't actually know modern C++" before it costs you a week on Project 1.
Every database is bigger than RAM, so it needs a component that shuttles fixed-size pages between disk and an in-memory pool, tracks which pages are pinned (in use) vs. evictable, and picks a victim to evict using an LRU-K replacement policy when the pool is full. Get this wrong and every layer built on top of it — the index, the query executor — silently corrupts data or deadlocks, which is exactly why it's project 1.
You implement a disk-based, thread-safe B+Tree — the structure almost every relational database uses for its indexes — supporting point lookups, range scans via leaf-node linked lists, and concurrent inserts/deletes using latch crabbing so multiple threads can safely walk and modify the tree at once. It's the project most students say taught them more about real data structures than an entire algorithms course did.
Project 3 has you build the Volcano-style iterator operators (sequential scan, joins, aggregation, sorting) that turn a query plan into actual results, one tuple at a time. Project 4 then makes it correct under concurrent transactions — implementing two-phase locking or multi-version concurrency control so overlapping readers and writers can't corrupt each other's view of the data. Finish both and you have a real, working relational database engine you wrote yourself.
Assignment 1 starts you on a single machine: you parallelize small programs (a Mandelbrot renderer, a sqrt approximator) first with ISPC, which compiles a single-program description down to SIMD instructions across the cores you have, then with raw threads — and you're graded partly on producing a speedup analysis, not just correct output. It's the fastest way to build real intuition for why "more cores" doesn't automatically mean "more speed."
You build a task execution library from scratch in C++ — the same category of system as a thread pool inside a game engine or a build system — implementing several increasingly sophisticated scheduling policies for task graphs with dependencies, and measuring how scheduling overhead and load balancing trade off against each other as task sizes shrink. This is where "parallel programming" stops being about calling a library and starts being about building one.
You write a parallel renderer in CUDA — circles composited onto a canvas — which sounds like graphics but is really a forcing function to understand thread blocks, warps, shared memory, and memory coalescing, because a naive implementation is both wrong (race conditions on overlapping pixels) and slow (uncoalesced global memory access) at the same time. This is most people's first real contact with how a GPU actually schedules and moves data, underneath the CUDA/PyTorch abstractions used daily in ML.
The final assignment moves to distributed-memory parallelism: graph algorithms like PageRank or BFS parallelized first with OpenMP on one machine, then with MPI across multiple machines that share nothing and must communicate explicitly by message-passing. It's a deliberate bridge — the programming model here (independent nodes, explicit messages, no shared state) is exactly what tier 05's distributed systems course is built on.
Lab 1 has you implement a MapReduce coordinator and worker pool from the original Google paper: the coordinator hands out map and reduce tasks over RPC, workers process them and write intermediate files, and — the actual point of the lab — the coordinator has to notice when a worker dies mid-task and re-assign that task without corrupting output. It's the gentlest possible introduction to the core distributed-systems problem: things fail, and your system has to keep working anyway.
Lab 3 is the heart of the course: implement Raft, the consensus algorithm behind etcd, CockroachDB, and Consul, across three stages — leader election with randomized timeouts, log replication with the AppendEntries protocol, and persistence plus log compaction via snapshots so a restarted server can recover state without replaying its entire history. Tests inject network partitions, dropped RPCs, and server crashes at every stage, so "it passes on my laptop" and "it's actually correct" are very different bars here.
With Raft working, Lab 4 puts a key/value service on top of it: clients send Put/Append/Get RPCs to whichever server they believe is leader, the service submits each operation to Raft, and only applies it — and replies to the client — once Raft confirms it's committed on a majority of replicas. You also have to handle duplicate client requests from retries without applying them twice, which is where "distributed systems are hard" stops being an abstraction and starts being a specific bug you're chasing.
The final lab splits the keyspace across multiple independent Raft groups, each owning a subset of shards, coordinated by a separate shard controller that can move shard ownership between groups while the system stays live. The hard part isn't sharding itself — it's migrating a shard from one Raft group to another without ever losing a write or serving a request against stale data during the handoff, which is essentially how real systems like CockroachDB or DynamoDB scale writes horizontally.
Berkeley's database course, structurally close to 15-445 but its own codebase (SimpleDB) and its own opinions on how to teach storage, indexing, query optimization, transactions, and recovery — worth doing after 15-445 specifically because a second implementation of the same ideas in a different codebase is what makes the concepts (not just "how BusTub happens to do it") actually stick.
A distributed systems course whose main project — a highly-available, fault-tolerant, transactional key-value store, built in Java over several team labs — was explicitly designed as a sibling to MIT's 6.824/6.5840 project. Same core problem, different language, different framing of the same consensus-and-replication ideas from tier 05.
Brown's distributed systems course builds up a distributed computing framework across seven milestones — centralized computing, serialization, actors & RPC, node groups & gossip, distributed storage, distributed processing, and cloud deployment — ending with the same destination as 6.5840 (a working distributed store you built) reached through a very different, more incremental path.