Learn / Rust Practical Systems
Rust Types, Traits, and Modules
A consolidated foundations lesson preserving 4 focused PTesting lessons without duplicating an unrelated authored PTLearn topic.
Course: Rust Practical Systems. Level: Intermediate. Topic: Systems reliability.
Stage: intermediate - Practice - Language and runtime foundations. Connect rust types, traits, and modules to the professional workflow for Rust Systems.
Outcomes
- Define structs with methods and associated functions
- Use enums to model alternatives and state
- Apply match, if let, while let, destructuring, and guards
- Define traits and implement them for concrete types
- Use generic functions, trait bounds, where clauses, and impl Trait
- Choose between static dispatch with generics and dynamic dispatch with trait objects
- Organize code with packages, crates, modules, and paths
- Use pub, pub(crate), re-exports, and prelude modules intentionally
- Split a growing codebase into maintainable crate boundaries
- Apply newtype, builder, typestate, and enum state-machine patterns
- Use traits for dependency boundaries without over-abstracting
- Recognize when Rust patterns replace inheritance-heavy designs
Concepts
- Structs, Enums, and Pattern Matching
- Structs group related data
- Enums model alternatives
- Rust for Reliable Systems and Services foundations
- Guided practice
- Traits and Generics
- Traits define shared behavior
- Generic bounds and dispatch
- Modules and Crates
- The namespace hierarchy
- Visibility is design
- Rust Design Patterns
- Newtypes and builders
- Typestate and enum state machines
Concept flow
Show how language and runtime foundations moves from trigger to implementation outcome in Rust Systems.
- Language model
- Runtime behavior
- Engineering decision
- Verification evidence
Session flow
- Model Structs, Enums, and Pattern Matching (concept, 113 min) — Name the decisions behind Structs, Enums, and Pattern Matching before writing code.
- Define structs with methods and associated functions
- Explain where Structs, Enums, and Pattern Matching belongs in job orchestration service.
- Build the vertical slice (walkthrough, 203 min) — Implement the smallest useful slice in legacy/rust-systems/rust-types-traits-and-modules.txt.
- Use enums to model alternatives and state
- Connect Structs group related data to a working example.
- Verify and harden (exercise, 135 min) — Accepted answers: Pending | Authorized | Captured | Rejected
- Apply match, if let, while let, destructuring, and guards
- Record one risk or follow-up before moving on.
- Structs, Enums, and Pattern Matching: Structs group related data (concept, 60 min) — Structs are product types: they combine fields into one named concept. Implementations attach methods, while associated functions such as new act as constructors.
- Retained source example: Struct with methods
#[derive(Debug, Clone)]
struct Money {
cents: i64,
currency: String,
}
impl Money {
fn new(cents: i64, currency: impl Into<String>) -> Self {
Self { cents, currency: currency.into() }
}
fn is_positive(&self) -> bool {
self.cents > 0
}
}
impl Into<String> lets callers pass either a String or a string literal.
- Structs, Enums, and Pattern Matching: Enums model alternatives (walkthrough, 60 min) — Enums are sum types: a value is exactly one variant at a time. Variants can carry no data, tuple-like data, or named fields. This makes illegal states harder to represent.
- Retained source example: State as an enum
enum JobState {
Queued,
Running { worker_id: String },
Succeeded,
Failed(String),
}
fn label(state: &JobState) -> String {
match state {
JobState::Queued => "queued".to_string(),
JobState::Running { worker_id } => format!("running on {worker_id}"),
JobState::Succeeded => "succeeded".to_string(),
JobState::Failed(reason) => format!("failed: {reason}"),
}
}
The match must account for every variant, so new states create useful compiler work.
- Structs, Enums, and Pattern Matching: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Prefer enums over strings or integer status codes for closed sets of states.
- Practice: Derive Debug for types you will inspect during development and tests.
- Practice: Keep constructors small and enforce invariants at creation boundaries.
- Avoid: Using bool pairs where an enum would better represent mutually exclusive states.
- Avoid: Adding wildcard match arms too early and hiding future variant handling.
- Avoid: Exposing public struct fields before deciding which invariants must be protected.
- Structs, Enums, and Pattern Matching: references (review, 2 min) — Original references retained from the legacy library.
- Rust book: structs: https://doc.rust-lang.org/book/ch05-00-structs.html
- Rust book: enums and pattern matching: https://doc.rust-lang.org/book/ch06-00-enums.html
- Traits, Generics, and Dispatch: Traits define shared behavior (concept, 60 min) — A trait is a contract for behavior. Types implement traits to participate in generic functions and APIs. The standard library uses traits heavily for formatting, conversion, iteration, error handling, ordering, and thread safety.
- Retained source example: A small trait
trait Render {
fn render(&self) -> String;
}
struct Heading(String);
impl Render for Heading {
fn render(&self) -> String {
format!("<h1>{}</h1>", self.0)
}
}
The Heading type can now be used by generic code that requires Render.
- Traits, Generics, and Dispatch: Generic bounds and dispatch (walkthrough, 60 min) — Generic functions are monomorphized by default, meaning the compiler generates specialized code for concrete types. Trait objects such as Box<dyn Render> use dynamic dispatch and are useful when a collection must hold multiple concrete types behind one interface.
- Retained source example: Static and dynamic dispatch
fn render_static<T: Render>(item: &T) -> String {
item.render()
}
fn render_dynamic(item: &dyn Render) -> String {
item.render()
}
The generic version is statically dispatched; the dyn version calls through a vtable.
- Traits, Generics, and Dispatch: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Keep traits focused; large traits are harder to implement and mock.
- Practice: Use impl Trait in arguments for simple APIs and named generics when relationships matter.
- Practice: Reach for dyn Trait when heterogeneous values or plugin-like behavior matter more than static dispatch.
- Avoid: Adding generic parameters that are never related to inputs or outputs.
- Avoid: Using trait objects before understanding object-safety limitations.
- Avoid: Over-abstracting early instead of starting with concrete types.
- Traits, Generics, and Dispatch: references (review, 2 min) — Original references retained from the legacy library.
- Rust book: traits: https://doc.rust-lang.org/book/ch10-02-traits.html
- Rust reference: trait objects: https://doc.rust-lang.org/reference/types/trait-object.html
- Module System and API Design: The namespace hierarchy (concept, 45 min) — A package contains one or more crates. A crate contains modules. Modules control paths and visibility. Files are a way to define modules, but the logical module tree is what matters to the compiler.
- Retained source example: Re-exporting a public API
// src/lib.rs
mod client;
mod error;
pub use client::ApiClient;
pub use error::ApiError;
// src/client.rs
pub struct ApiClient {
base_url: String,
}
Internal module layout can stay private while lib.rs exposes a stable public surface.
- Module System and API Design: Visibility is design (walkthrough, 45 min) — Rust items are private by default. Public APIs should be smaller than internal structure. Use pub(crate) for crate-wide internals and re-export only names that callers should depend on.
- Retained source example: Limited visibility
pub struct User {
id: u64,
pub email: String,
}
impl User {
pub fn new(id: u64, email: String) -> Self {
Self { id, email }
}
pub fn id(&self) -> u64 {
self.id
}
}
The id field is private, but callers can read it through a method that preserves invariants.
- Module System and API Design: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Design public APIs from the caller's perspective, not from the file layout.
- Practice: Keep module names domain-oriented instead of dumping unrelated helpers into utils.
- Practice: Use integration tests to exercise the crate as an external caller would.
- Avoid: Making fields public before deciding invariants.
- Avoid: Mirroring every file path in the public API.
- Avoid: Creating many tiny crates before module boundaries have stabilized.
- Module System and API Design: references (review, 2 min) — Original references retained from the legacy library.
- Rust book: packages and crates: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html
- Cargo workspaces: https://doc.rust-lang.org/cargo/reference/workspaces.html
- Idiomatic Rust Patterns: Newtypes and builders (concept, 60 min) — A newtype wraps an existing type to create a distinct domain meaning. Builders are useful when construction has many optional fields, validation, or staged defaults.
- Retained source example: Newtype for validated email
#[derive(Debug, Clone, PartialEq, Eq)]
struct Email(String);
impl Email {
fn parse(value: String) -> Result<Self, String> {
if value.contains('@') {
Ok(Self(value))
} else {
Err("email must contain @".to_string())
}
}
fn as_str(&self) -> &str {
&self.0
}
}
After construction, Email carries a stronger invariant than a raw String.
- Idiomatic Rust Patterns: Typestate and enum state machines (walkthrough, 60 min) — Typestate encodes allowed transitions in types, so invalid calls do not compile. Enum state machines are simpler when the state changes dynamically and must be inspected at runtime.
- Retained source example: Typestate sketch
struct Draft;
struct Published;
struct Article<State> {
title: String,
state: std::marker::PhantomData<State>,
}
impl Article<Draft> {
fn publish(self) -> Article<Published> {
Article { title: self.title, state: std::marker::PhantomData }
}
}
Only draft articles have the publish method, so publishing an already published article is not representable.
- Idiomatic Rust Patterns: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Use newtypes at boundaries where raw primitives would obscure meaning or invariants.
- Practice: Use builders when construction complexity is real, not as default ceremony.
- Practice: Prefer enum state machines for dynamic workflow state and typestate for strict compile-time protocols.
- Avoid: Recreating object-oriented inheritance hierarchies instead of using enums and traits.
- Avoid: Using typestate where runtime state inspection would be simpler.
- Avoid: Adding traits before there are multiple meaningful implementations or a test boundary.
- Idiomatic Rust Patterns: references (review, 2 min) — Original references retained from the legacy library.
- Rust design patterns: https://rust-unofficial.github.io/patterns/
- PhantomData: https://doc.rust-lang.org/std/marker/struct.PhantomData.html
Code example
rust in legacy/rust-systems/rust-types-traits-and-modules.txt.
fn main() {}
Walkthrough examples
- Rust Types, Traits, and Modules in a job orchestration service — A team is extending an Axum service with a companion CLI and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: legacy/rust-systems/rust-types-traits-and-modules.txt
- File: tests/rust-types-traits-and-modules.spec
- File: docs/rust-systems/rust-types-traits-and-modules.md
- Start from the provided rust snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Explain the language or runtime rule in your own words" before adding extra behavior.
- Write down how the implementation changes when Structs group related data fails or becomes slow.
- Struct with methods — impl Into<String> lets callers pass either a String or a string literal.
- Retained source code:
#[derive(Debug, Clone)]
struct Money {
cents: i64,
currency: String,
}
impl Money {
fn new(cents: i64, currency: impl Into<String>) -> Self {
Self { cents, currency: currency.into() }
}
fn is_positive(&self) -> bool {
self.cents > 0
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- State as an enum — The match must account for every variant, so new states create useful compiler work.
- Retained source code:
enum JobState {
Queued,
Running { worker_id: String },
Succeeded,
Failed(String),
}
fn label(state: &JobState) -> String {
match state {
JobState::Queued => "queued".to_string(),
JobState::Running { worker_id } => format!("running on {worker_id}"),
JobState::Succeeded => "succeeded".to_string(),
JobState::Failed(reason) => format!("failed: {reason}"),
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Option with if let — if let is concise when one pattern matters more than the exhaustive list.
- Retained source code:
fn print_port(port: Option<u16>) {
if let Some(port) = port {
println!("listening on {port}");
} else {
println!("port not configured");
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- A small trait — The Heading type can now be used by generic code that requires Render.
- Retained source code:
trait Render {
fn render(&self) -> String;
}
struct Heading(String);
impl Render for Heading {
fn render(&self) -> String {
format!("<h1>{}</h1>", self.0)
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Static and dynamic dispatch — The generic version is statically dispatched; the dyn version calls through a vtable.
- Retained source code:
fn render_static<T: Render>(item: &T) -> String {
item.render()
}
fn render_dynamic(item: &dyn Render) -> String {
item.render()
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- where clauses keep signatures readable — A where clause scales better when bounds become longer or involve multiple type parameters.
- Retained source code:
use std::fmt::Display;
fn join_display<T>(items: &[T], separator: &str) -> String
where
T: Display,
{
items.iter().map(ToString::to_string).collect::<Vec<_>>().join(separator)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Re-exporting a public API — Internal module layout can stay private while lib.rs exposes a stable public surface.
- Retained source code:
// src/lib.rs
mod client;
mod error;
pub use client::ApiClient;
pub use error::ApiError;
// src/client.rs
pub struct ApiClient {
base_url: String,
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Limited visibility — The id field is private, but callers can read it through a method that preserves invariants.
- Retained source code:
pub struct User {
id: u64,
pub email: String,
}
impl User {
pub fn new(id: u64, email: String) -> Self {
Self { id, email }
}
pub fn id(&self) -> u64 {
self.id
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Workspace manifest — Workspaces share target output and dependency resolution across related packages.
- Retained source code:
[workspace]
members = [
"crates/domain",
"crates/api",
"crates/cli",
]
resolver = "2"
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Newtype for validated email — After construction, Email carries a stronger invariant than a raw String.
- Retained source code:
#[derive(Debug, Clone, PartialEq, Eq)]
struct Email(String);
impl Email {
fn parse(value: String) -> Result<Self, String> {
if value.contains('@') {
Ok(Self(value))
} else {
Err("email must contain @".to_string())
}
}
fn as_str(&self) -> &str {
&self.0
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Typestate sketch — Only draft articles have the publish method, so publishing an already published article is not representable.
- Retained source code:
struct Draft;
struct Published;
struct Article<State> {
title: String,
state: std::marker::PhantomData<State>,
}
impl Article<Draft> {
fn publish(self) -> Article<Published> {
Article { title: self.title, state: std::marker::PhantomData }
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Trait boundary for testable services — The service depends on behavior, allowing tests to provide a deterministic clock.
- Retained source code:
trait Clock {
fn now_ms(&self) -> u64;
}
struct TokenService<C> {
clock: C,
}
impl<C: Clock> TokenService<C> {
fn issued_at(&self) -> u64 {
self.clock.now_ms()
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Model a payment status: Sketch enum variants for a payment that can be Pending, Authorized with an id, Captured, or Rejected with a reason. The guided runner checks for variant names and does not compile code.
- Starter code: enum PaymentStatus {
}
- Hint: Use a struct-like variant for Authorized if you want a named id field.
- Hint: Rejected should carry a reason String.
- Reference solution: enum PaymentStatus { Pending, Authorized { id: String }, Captured, Rejected(String) }
- Accepted answers: Pending | Authorized | Captured | Rejected
- Select a trait bound: A function prints values with println!("{}", value). Which standard trait bound is needed? The guided runner checks the trait name.
- Starter code: trait bound:
- Hint: Debug uses {:?}.
- Hint: Display uses {}.
- Reference solution: trait bound: std::fmt::Display
- Accepted answers: Display | std::fmt::Display
- Pick visibility for an internal helper: A helper function is used by several modules in the same crate but should not be public to downstream users. Choose private, pub(crate), or pub. The guided runner checks the chosen visibility.
- Starter code: visibility:
- Hint: Private is module-only.
- Hint: pub exposes it to external crates.
- Hint: pub(crate) exposes it inside the current crate.
- Reference solution: visibility: pub(crate)
- Accepted answers: pub(crate)
- Select the Rust pattern: Choose newtype, builder, typestate, or enum state machine for: validated user id, many optional configuration fields, compile-time connection handshake, runtime job status. The guided runner checks terms.
- Starter code: validated id:
optional config:
compile-time handshake:
runtime job status:
- Hint: Newtype gives a raw value domain meaning.
- Hint: Typestate moves state transitions into the type system.
- Reference solution: validated id: newtype
optional config: builder
compile-time handshake: typestate
runtime job status: enum state machine
- Accepted answers: newtype | builder | typestate | enum
Checklist
- Explain the language or runtime rule in your own words
- Run one focused example and record its output
- Name one failure mode or tradeoff
- Keep the verification evidence with the lesson
Quiz prompts
- A teammate wants to hide Structs, Enums, and Pattern Matching inside a convenient helper. What should you check first? — Place Structs, Enums, and Pattern Matching at the boundary that keeps job orchestration service behavior explicit, testable, and reviewable.
- Which artifact best proves this Rust Systems lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Why are enums useful for domain modeling? — Enums make alternatives explicit and pair naturally with exhaustive pattern matching.
- What is monomorphization? — Rust usually compiles generic code into concrete specialized versions, enabling static dispatch and optimization.
- What is a re-export commonly used for? — pub use lets a crate present a clean API without forcing callers to know internal module paths.
- What is the main benefit of the newtype pattern? — Newtypes let the compiler distinguish values that would otherwise share a primitive representation.
Flashcards
- In Rust Systems, what should you remember about Structs, Enums, and Pattern Matching? Structs, Enums, and Pattern Matching matters here because it supports "Define structs with methods and associated functions".
- In Rust Systems, what should you remember about Structs group related data? Structs group related data matters here because it supports "Use enums to model alternatives and state".
- In Rust Systems, what should you remember about Enums model alternatives? Enums model alternatives matters here because it supports "Apply match, if let, while let, destructuring, and guards".
- In Rust Systems, what should you remember about Rust for Reliable Systems and Services foundations? Rust for Reliable Systems and Services foundations matters here because it supports "Define traits and implement them for concrete types".
Labs
- Ship a rust types, traits, and modules slice — Extend an Axum service with a companion CLI with a small but reviewable feature that proves the lesson's architecture in code.
- Model a payment status: Sketch enum variants for a payment that can be Pending, Authorized with an id, Captured, or Rejected with a reason. The guided runner checks for variant names and does not compile code.
- Starter code: enum PaymentStatus {
}
- Hint: Use a struct-like variant for Authorized if you want a named id field.
- Hint: Rejected should carry a reason String.
- Reference solution: enum PaymentStatus { Pending, Authorized { id: String }, Captured, Rejected(String) }
- Accepted answers: Pending | Authorized | Captured | Rejected
- The implementation demonstrates Structs, Enums, and Pattern Matching without hidden global state.
- At least one test or verification step covers the main success path.
- The review notes explain the tradeoff behind Structs group related data.
Challenge
- Review-ready rust types, traits, and modules (Stretch) — Turn the lesson work into a pull-request-sized change for job orchestration service. Include the code, verification notes, one explicit tradeoff, and an updated concept diagram that names the riskiest handoff.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from legacy/rust-systems/rust-types-traits-and-modules.txt plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL