Chapter 01 — Foundation

WHY RUST?
THE CASE.

~70% of CVEs in large C/C++ codebases are memory safety bugs. Rust eliminates the entire class at compile time — no garbage collector, no runtime overhead, C-level speed. Safety without compromise.

The Problem Rust Solves

Memory safety bugs — use-after-free, buffer overflow, null pointer dereference, data races — have plagued C and C++ for 50 years. They account for the majority of critical CVEs in browsers, operating systems, and servers. Rust's ownership model prevents all of them at compile time. No GC. No runtime. Zero-cost.

Real impact: Microsoft says ~70% of their security bugs are memory safety issues. Google Chrome: same number. The Android team rewrote critical components in Rust and saw memory safety vulnerabilities drop to nearly zero in those components.

Rust vs Go vs C++: When to Use Each

The Go + Rust Combo

Go handles the service layer — HTTP APIs, goroutine concurrency, orchestration. Rust handles the compute layer — data parsing, encoding/decoding, hot-path algorithms. Call Rust from Go via CGo or compile Rust to WASM and call from anywhere. This is the stack of the next 10 years.

Who uses Rust: AWS (Firecracker VMM), Microsoft (Windows components), Google (Android, Fuchsia), Cloudflare (edge runtime), Discord (replaced Elixir for lower latency), Linux kernel (since 6.1).

Hello, Rust

use std::collections::HashMap;

fn word_count(text: &str) -> HashMap<&str, usize> {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    counts
}

fn main() {
    let text = "go is fast rust is safe both are good";
    let counts = word_count(text);
    println!("{:?}", counts);
    // Compiles to a 200KB binary. No runtime. Starts instantly.
}
Chapter 02 — Foundation

OWNERSHIP
& BORROWING

Rust's ownership system is its defining feature. Three rules: each value has one owner, ownership can be transferred (moved), and you can borrow references — either one mutable or many immutable. The compiler enforces all three at compile time.

The Three Rules

Move Semantics

let s1 = String::from("hello");
let s2 = s1;  // s1 is MOVED into s2
println!("{}", s1);  // COMPILE ERROR: value moved

// Fix 1: Clone (deep copy)
let s3 = s2.clone();

// Fix 2: Borrow (reference)
let s4 = &s2;  // s2 still owns the data
println!("{} {}", s2, s4);  // both work

Borrowing in Practice

fn calculate_total(orders: &[Order]) -> f64 {
    // takes immutable borrow — caller still owns orders
    orders.iter().map(|o| o.price * o.qty as f64).sum()
}

fn apply_discount(orders: &mut Vec<Order>, pct: f64) {
    // mutable borrow — exclusive access
    for o in orders.iter_mut() {
        o.price *= 1.0 - pct;
    }
}
Why this matters: Data races require two concurrent accesses where at least one is a write. Rust's borrow rules make data races impossible — the compiler won't compile code that could race. No mutex needed for safe concurrent read access.

Lifetimes: References Must Not Outlive Data

// This won't compile — reference outlives the data
fn dangling() -> &str {
    let s = String::from("hello");
    &s  // ERROR: s is dropped here, reference would dangle
}

// This is fine — the reference borrows from the caller's data
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}
Chapter 03 — Foundation

RESULT,
OPTION &
ERROR

Rust has no null and no exceptions. Option<T> replaces null. Result<T, E> replaces exceptions. Both are enums — the compiler forces you to handle all cases. The ? operator makes it ergonomic.

Option: No Null Pointer Exceptions — Ever

fn find_user(id: u64) -> Option<User> {
    db.get(&id).cloned()  // Some(user) or None
}

match find_user(42) {
    Some(user) => println!("Found: {}", user.name),
    None       => println!("Not found"),
}

// Or use ? in functions returning Option
let name = find_user(42)?.name;

Result: Errors Are Values

use std::fs;

fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let data = fs::read_to_string(path)?;  // ? propagates error
    let cfg: Config = serde_json::from_str(&data)?;
    Ok(cfg)
}

// ? is sugar for:
// let data = match fs::read_to_string(path) {
//     Ok(d)  => d,
//     Err(e) => return Err(e.into()),
// };
Zero-cost error handling: No exception tables, no stack unwinding overhead for the happy path. Result is just a discriminated union. The ? operator compiles to a conditional branch — same as manual if let Err.
Chapter 04 — Foundation

TRAITS
& GENERICS

Traits are Rust's interfaces. Generics + trait bounds give you zero-cost polymorphism — the compiler monomorphizes code at compile time, so there's no virtual dispatch overhead.

Defining and Implementing Traits

trait Serialize {
    fn serialize(&self) -> Vec<u8>;
    fn size_hint(&self) -> usize { 0 }  // default impl
}

struct Order { id: u64, symbol: String, qty: i32 }

impl Serialize for Order {
    fn serialize(&self) -> Vec<u8> {
        format!("{}:{}:{}", self.id, self.symbol, self.qty)
            .into_bytes()
    }
}

Generics with Trait Bounds

// Works for any T that implements Serialize — zero runtime overhead
fn write_to_disk<T: Serialize>(item: &T, path: &str) -> Result<(), io::Error> {
    let bytes = item.serialize();
    fs::write(path, bytes)
}

// Dynamic dispatch when type isn't known at compile time
fn write_any(item: &dyn Serialize, path: &str) -> Result<(), io::Error> {
    fs::write(path, item.serialize())
}
Static vs Dynamic dispatch: Generic <T: Trait> = monomorphization = zero overhead. dyn Trait = vtable = small runtime cost. Use generics for hot paths, dyn for plugin/extension points.
Chapter 05 — Build It

BUILD:
HTTP SERVER
FROM SCRATCH

Same CodeCrafters challenge as the Go version — but in Rust. You'll feel the difference: ownership forces you to think about connection lifetime, buffer ownership, and error propagation more explicitly.
PROJECT: Rust HTTP/1.1 Server
Estimated time: 4–5 hours · Skills: TcpListener, ownership, error handling
TCP listener → request parser → router → response writer. Concurrent connections via threads or (extension) Tokio async.
TcpListener::bind("0.0.0.0:4221") — accept loop
thread::spawn for each connection (or tokio::spawn)
BufReader::new(stream) — read lines until empty line
Parse request line: split by space, extract method/path/version
Route: match path prefix, write HTTP response to stream
Support /echo/{str}: write str back as body with Content-Length
Support gzip: check Accept-Encoding, compress body with flate2
use std::net::{TcpListener, TcpStream};
use std::io::{BufRead, BufReader, Write};
use std::thread;

fn main() {
    let listener = TcpListener::bind("0.0.0.0:4221").unwrap();
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        thread::spawn(|| handle(stream));
    }
}

fn handle(mut stream: TcpStream) {
    let reader = BufReader::new(&stream);
    let request: Vec<String> = reader.lines()
        .map(|l| l.unwrap())
        .take_while(|l| !l.is_empty())
        .collect();
    let path = parse_path(&request[0]);
    let response = route(path);
    stream.write_all(response.as_bytes()).unwrap();
}
Ownership challenge: Notice we borrow &stream for BufReader but need mut stream to write. Rust forces you to think about this — the solution (split or scope the borrow) teaches you something you'll use everywhere.
Chapter 06 — Build It

BUILD:
DNS
RESOLVER

DNS is a binary protocol over UDP. Building a resolver teaches you: binary packet parsing, bitfield manipulation, recursive query forwarding. Pure byte manipulation — where Rust's safety shines.
PROJECT: DNS Resolver
Estimated time: 6–8 hours · Skills: UDP, binary parsing, bitfields
Parse DNS query packets, forward to 8.8.8.8, parse response, return to client. Implement A, CNAME record types.
UdpSocket::bind on port 2053, receive DNS query bytes
Parse DNS header: 12 bytes, bitfields for QR/OPCODE/AA/TC/RD/RA
Parse question section: length-prefixed labels (3www6google3com)
Forward query to 8.8.8.8:53, receive response
Parse answer section: NAME/TYPE/CLASS/TTL/RDLENGTH/RDATA
Return response bytes to original client
struct DnsHeader {
    id:      u16,
    flags:   u16,  // QR(1) OPCODE(4) AA(1) TC(1) RD(1) RA(1) Z(3) RCODE(4)
    qdcount: u16,
    ancount: u16,
    nscount: u16,
    arcount: u16,
}

impl DnsHeader {
    fn parse(buf: &[u8]) -> Self {
        Self {
            id:      u16::from_be_bytes([buf[0], buf[1]]),
            flags:   u16::from_be_bytes([buf[2], buf[3]]),
            qdcount: u16::from_be_bytes([buf[4], buf[5]]),
            ancount: u16::from_be_bytes([buf[6], buf[7]]),
            nscount: u16::from_be_bytes([buf[8], buf[9]]),
            arcount: u16::from_be_bytes([buf[10], buf[11]]),
        }
    }
    fn is_response(&self) -> bool { self.flags & 0x8000 != 0 }
}
Chapter 07 — Build It

BUILD:
UNIX
SHELL

A shell is a REPL that spawns processes. Build one that handles builtin commands, pipes, redirections, and signal handling. Classic systems programming — perfect for Rust.
PROJECT: MiniShell
Estimated time: 5–6 hours · Skills: process::Command, pipes, builtins
REPL: read line from stdin, tokenize, execute
Builtins: echo, cd, pwd, exit, type (no fork needed)
External commands: std::process::Command::new("ls").spawn()
PATH resolution: find executable in PATH env var
Pipes: cmd1 | cmd2 — stdout of cmd1 to stdin of cmd2
Redirections: cmd > file.txt, cmd >> file.txt, cmd < input.txt
use std::process::Command;
use std::io::{self, Write};

fn main() {
    loop {
        print!("$ ");
        io::stdout().flush().unwrap();
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();
        let parts: Vec<&str> = input.trim().split_whitespace().collect();
        if parts.is_empty() { continue; }
        match parts[0] {
            "exit" => break,
            "cd"   => { /* handle cd */ },
            cmd    => {
                Command::new(cmd).args(&parts[1..]).spawn()
                    .map(|mut c| c.wait()).ok();
            }
        }
    }
}
Chapter 08 — Build It

BUILD:
SQLITE
ENGINE

SQLite stores data in a B-tree format on disk. Building a reader teaches you page-based storage, B-tree traversal, record encoding, and SQL parsing. The hardest project here — and the most rewarding.
PROJECT: SQLite Reader
Estimated time: 10–15 hours · Skills: B-tree, page parsing, SQL, file I/O
Read a real .db SQLite file. Parse pages, traverse B-trees, decode records, execute SELECT queries. Use the real SQLite file format spec.
Read the 100-byte database header: page size, encoding, version
Parse page header: page type (table leaf / interior / index)
Read cell pointers array, parse cell content areas
Decode records: varint-encoded column types + values
Traverse B-tree: follow interior node pointers to leaf pages
Parse sqlite_master table to get schema: table names, columns
Execute SELECT col FROM table WHERE col = val
Why this is hard and worth it: Every modern database uses a variant of this structure. Engineers who've implemented B-tree traversal understand why indexes exist, what a full table scan costs, and why column ordering in indexes matters. This knowledge translates directly to writing better queries.
Chapter 09 — Advanced

ASYNC
WITH TOKIO

Rust's async/await compiles to state machines — zero heap allocation per await point compared to Go goroutines (which use 2KB stacks). Tokio is the production async runtime: used by AWS, Discord, Cloudflare.

The Basics

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
    loop {
        let (socket, _) = listener.accept().await.unwrap();
        tokio::spawn(async move {
            handle(socket).await;
        });
    }
}
Go vs Rust async: Go goroutines are simpler — just go fn(). Rust async requires explicit .await and the async keyword propagates upward. But Rust async compiles to zero-overhead state machines. For I/O-bound services, both are excellent. For CPU-bound work, Rust's rayon parallelism wins.
Chapter 10 — Advanced

WASM:
RUST IN
THE BROWSER

Compile Rust to WebAssembly. Run compute-heavy code in the browser at near-native speed. No JavaScript rewrite. This is how InvesTar runs its trading engine client-side.

Setup and Build

cargo install wasm-pack
wasm-pack build --target web
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn calculate_signals(prices: &[f64]) -> f64 {
    // Runs in browser at near-native speed
    // Same code runs server-side and client-side
    let sma: f64 = prices.iter().sum:: <f64>() / prices.len() as f64;
    sma
}
Real-world use: Figma's rendering engine, AutoCAD web, Google Earth Web, InvesTar's WASM trading logic. Anything that needs real compute in the browser without a server roundtrip.
Chapter 11 — Advanced

GO + RUST
TOGETHER

The full-stack combination: Go owns the service layer, Rust owns the compute layer. Call Rust from Go via CGo, or compile to shared library, or via WASM. The combination is the stack of elite infrastructure teams.

Pattern 1: Rust as a Shared Library

// lib.rs — compile with: cargo build --release --lib
#[no_mangle]
pub extern "C" fn compute_sma(prices: *const f64, len: usize) -> f64 {
    unsafe {
        let slice = std::slice::from_raw_parts(prices, len);
        slice.iter().sum::<f64>() / len as f64
    }
}
// main.go — call the Rust library via CGo
// #cgo LDFLAGS: -L./rust/target/release -lcompute
// #include "compute.h"
import "C"

func SMA(prices []float64) float64 {
    return float64(C.compute_sma(
        (*C.double)(&prices[0]),
        C.size_t(len(prices)),
    ))
}

Architecture: Where Each Lives

This is what we build at SERVLOCI. InvesTar uses this exact architecture: Go API server + Rust WASM trading engine running in the browser. Engineers who understand both are the most valuable in the market.
READY TO LEARN WITH US?
See Training Program → Read Go Book →