Habits of mind for writing Rust that is correct, idiomatic, fast, and maintainable.
Rust is not just “C with a borrow checker.” It is a language with its own design pressure: ownership replaces manual memory management, Result replaces exceptions, traits replace inheritance, and unsafe is a proof obligation rather than a permission slip. These mentalities are written for Rust specifically — each one names Rust types, crates, tools, and failure modes.
Entries are grouped by domain, not ranked. Read the domain you touch most, then follow the boundaries into the layers around it.
Foundations: Ownership and Borrowing · Lifetimes and References · Types and Invariants · Errors and Option · Memory and Allocation
Systems: Unsafe and FFI · Concurrency · Async and Runtimes · Performance and Iterators · Embedded and Systems Constraints
Design: API and Trait Design · Macros and Metaprogramming · Serialization and Data Modeling · Networking and I/O · Security and Cryptography
Practice: Testing, Debugging, and Verification · Tooling, Build, and Ecosystem · Observability and Operations · Project Structure and Maintenance
Reference: Index · Self-audit · Review lens · Anti-patterns · How to share · Four-week plan · Further reading · Contributing
| Mode | What it looks like | Cadence |
|---|---|---|
| Adopt | Pick one mentality. Apply it to a real crate or pull request. | One per week |
| Share | Cite the entry by name in review, or run a 15-minute brown bag. | One per sprint |
| Relearn | Answer the relearn question from memory, then teach it. | One per month |
Three rules:
- One mentality at a time. Depth changes code; breadth changes nothing.
- A mentality without a diff is a slogan. Every entry must alter a real decision.
- If you cannot name the Rust cost, you have not adopted it. “It compiles” is not an argument.
Each entry has four parts: a mantra, a practice to adopt, a smell that means it is missing, and a question that exposes whether you understand it. Examples assume stable Rust; nightly-only features are called out explicitly.
Read bottom-up to go from ownership to ecosystem. Read top-down to trace a compile error or production bug to its design cause.
| Domain | Core question |
|---|---|
| Ownership and Borrowing | Who owns this, and who may borrow it? |
| Lifetimes and References | What keeps this data alive? |
| Types and Invariants | Can the wrong thing even compile? |
Errors and Option |
Is this absent, failed, or a bug? |
| Memory and Allocation | Who allocates, and when is it freed? |
| Unsafe and FFI | What invariant makes this sound? |
| Concurrency | What can run at the same time, and what is shared? |
| Async and Runtimes | What happens while this task waits? |
| Performance and Iterators | What does this abstraction cost? |
| Embedded and Systems Constraints | What does the platform not give me? |
| API and Trait Design | What have I promised to callers? |
| Macros and Metaprogramming | Is this syntax or code generation? |
| Serialization and Data Modeling | What have I promised to every reader? |
| Networking and I/O | What happens when the peer misbehaves? |
| Security and Cryptography | Who can reach it, and what stops them? |
| Testing, Debugging, and Verification | How do I know it works? |
| Tooling, Build, and Ecosystem | Can I reproduce and trust the build? |
| Observability and Operations | Can I see what it is doing in production? |
| Project Structure and Maintenance | Can someone else change it safely? |
If the catalog feels large, begin with these. They prevent the most common Rust design mistakes.
Borrow, don’t clone · Make illegal states unrepresentable · Result is the default · unsafe is a proof obligation · Send and Sync are contracts · Do not block the executor · Profile before optimizing · Accept borrowed, return owned · Miri, loom, and sanitizers · Treat the network as unreliable
Then add the domain you touch most: ownership, async, unsafe, embedded, or API design.
The borrow checker is not an obstacle; it is a design reviewer that never gets tired.
A clone is a design decision, not a compiler appeasement.
- Adopt: Pass
&Tor&mut T; clone only for isolation, ownership transfer, or measured simplicity. Name the reason in review. - Smell:
.clone()added to silence a borrow error; clones inside hot loops; cloning before a function that could take ownership. - Relearn: Is this clone buying independence, or hiding an ownership design problem?
Transfer ownership; let the old binding die.
- Adopt: Move values into functions and structs. Use
std::mem::takeorreplacewhen you need to move out of a borrowed place. - Smell: Cloning before passing to a function that could accept ownership; fighting move errors with
.clone(). - Relearn: Who should own this value after the call?
Shared ownership is a tool, not a default.
- Adopt: Model ownership in structs. Use
RcorArconly when shared ownership is genuinely required; prefer borrowing otherwise. - Smell:
Rc<RefCell<T>>everywhere;Arc<Mutex<T>>for data only one task touches. - Relearn: Does this value need shared ownership, or just a borrow?
If two fields are independent, borrow them independently.
- Adopt: Destructure structs into disjoint fields; write methods that borrow only the fields they need; use free functions when
&mut selfis too broad. - Smell: A
&mut selfmethod that needs two fields and fails to compile; reaching forRefCellto work around field-level borrowing. - Relearn: Which fields are actually borrowed at the same time?
Cell,RefCell,Mutex, and atomics change the rules — say why.
- Adopt: Use
Cell/RefCellfor single-threaded interior mutation,Mutex/RwLock/atomics for shared state, and keep the escape hatch encapsulated. - Smell:
RefCellruntime panics in production;Mutexaround single-threaded data; interior mutability leaking through public APIs. - Relearn: Why can’t this be
&mut, and what runtime cost am I accepting?
Cleanup happens on every path, including the ones you did not write.
- Adopt: Use RAII guards for files, locks, and transactions. Implement
Dropfor resources, but do not rely on it for async cleanup or fallible work. - Smell: Manual
close()on the happy path; leaked file descriptors;Dropthat panics;mem::forgetused casually. - Relearn: What happens when this value is dropped, and on which paths?
An
Arcis a lifetime decision disguised as a pointer.
- Adopt: Prefer a single owner plus borrowing. Introduce
Arconly at a true sharing boundary, and document why the owner cannot be singular. - Smell:
Arcin every struct;Rccycles that never drop; strong counts that only grow. - Relearn: Could a single owner and a reference replace this
Arc?
Lifetimes describe relationships. They do not create lifetime.
&'a Tmeans “borrow valid for'a,” not “make it live longer.”
- Adopt: Fix the owner, not the annotation. Read lifetime errors as “this reference outlives its data,” then change the ownership or scope.
- Smell: Adding
'aeverywhere to silence the compiler; lifetime parameters that do not express a real relationship. - Relearn: Which value actually owns the data, and how long does it live?
Borrow internally; own at the edge.
- Adopt: Return
StringorVec<T>from public APIs when the caller needs ownership. Accept&str,&[T], andimpl AsRef<T>on the way in. - Smell: Returning
&strtied to a temporary; lifetime gymnastics across module boundaries;&Stringand&Vec<T>parameters. - Relearn: Does the caller need a borrow or ownership?
A reference is only as good as its owner.
- Adopt: Keep the owner alive for the whole borrow. Use
ArcorCowwhen data must be shared or conditionally owned. Never return a reference to a local. - Smell:
return &local; dangling references hidden behindunsafe; storing references in long-lived structs without a plan. - Relearn: What keeps this data alive while the reference exists?
&'static TandT: 'staticare different promises.
- Adopt: Distinguish a static borrow from a type that owns all its data. Use
Box::leakonly for genuinely process-lifetime data. - Smell:
Box::leakto satisfy'static;'staticbounds added everywhere; leaking memory to spawn a task. - Relearn: Does this need to live forever, or just own its data?
Lifetimes and generics have subtyping rules; wrappers can change them.
- Adopt: Understand covariance, contravariance, and invariance. Use
PhantomDatato control variance and auto traits deliberately. - Smell: Unexpected lifetime errors in generic wrappers;
PhantomDatacopied without understanding;unsafevariance assumptions. - Relearn: What variance does this type have, and why?
A value that borrows itself cannot move casually.
- Adopt: Prefer a redesign that avoids self-reference. If you need it, use
Pin,self_cell, orouroborosand understand the safety conditions. - Smell: Hand-rolled self-referential structs;
Pinused as a buzzword; unsafe code that assumes a stable address without enforcing it. - Relearn: Can I avoid the self-reference, or do I need a pinned owner?
The shortest borrow is the easiest borrow.
- Adopt: Scope borrows tightly with blocks; end a borrow before mutating; let non-lexical lifetimes do their job instead of cloning the world.
- Smell: A long-lived borrow that forces a clone of the whole struct; “cannot borrow as mutable” worked around by cloning.
- Relearn: What is the shortest scope that satisfies this borrow?
Lend the reference again; do not surrender it.
- Adopt: Pass
&*xor&mut *xto reborrow explicitly. Let the compiler insert reborrows when the types allow it. - Smell: A reference moved and then needed again; cloning to keep using a value after passing it.
- Relearn: Do I need to move this reference, or just lend it again?
The cheapest bug is the one the compiler refuses to accept.
Use the type system as the first test suite.
- Adopt: Use enums for alternatives and structs for “all fields together.” Make invalid combinations impossible to construct; validate at the boundary.
- Smell:
Optionfields that must beSometogether;is_runningandis_failedbooleans that can both be true; validation repeated in every caller. - Relearn: Which combination of fields is nonsense, and why does the type allow it?
A raw
u64does not say what it means.
- Adopt: Use
struct UserId(u64),struct Meters(f64), andstruct Email(String). Implement only the traits that make sense. - Smell: Raw integers for every ID; unit mixups;
Stringfor every semantic string. - Relearn: What does this primitive actually mean, and can the type say it?
A state is not a bag of booleans.
- Adopt: Model states as an enum and match exhaustively. Let the compiler tell you when a new variant needs handling.
- Smell:
is_running,is_paused,is_failed;matchwith a catch-all that hides new variants. - Relearn: What are the legal states, and what happens in each?
Turn untrusted input into a type that cannot be wrong.
- Adopt: Implement
TryFrom,FromStr, or a constructor that returnsResult<Validated, Error>. Pass the validated type inward. - Smell:
validate()returningbool; rawStringpassed through ten layers; checks repeated at every boundary. - Relearn: What type proves this input is valid on the other side?
If a call is only valid in one state, encode the state in the type.
- Adopt: Use marker types such as
Connecting,Connected, andClosed. Consumeselfon transitions so invalid sequences cannot compile. - Smell: Runtime checks for states the compiler could enforce; “you must call
start()first” comments. - Relearn: Can the compiler prevent this call in the wrong state?
A public trait is a promise about downstream implementations.
- Adopt: Use a private supertrait to seal traits that are not meant to be implemented externally. Document the extension policy.
- Smell: Public traits that are de facto sealed; downstream impls blocking a breaking change; adding methods to widely implemented traits.
- Relearn: Should downstream crates be able to implement this trait?
If the length is known at compile time, say so in the type.
- Adopt: Use const generics for array lengths, fixed dimensions, and buffer sizes. Let the compiler reject mismatched shapes.
- Smell: Runtime length checks that could be const;
Vec<T>where[T; N]would encode the invariant; dimension mixups caught by tests. - Relearn: What part of this shape is known at compile time?
A marker type is a claim about meaning, not decoration.
- Adopt: Use
PhantomDatato encode units, ownership, variance, and auto-trait behavior. Document what the marker means. - Smell:
PhantomDatacopied without understanding; variance bugs in wrappers; accidentalSend/Syncleakage. - Relearn: What does this marker change about the type, and why is it sound?
A value that occupies no space can still prove something.
- Adopt: Use zero-sized tokens as capabilities. Make their constructors private so holding one means the invariant was checked.
- Smell: A ZST token anyone can construct; a type-level “proof” with no invariant behind it.
- Relearn: What does holding this value prove, and who can create it?
Rust makes failure visible. Keep it that way.
Failure is part of the signature, not an interruption.
- Adopt: Return
Result<T, E>. Usethiserrorfor libraries andanyhowat application boundaries. Keep error types meaningful. - Smell:
Box<dyn Error>in public APIs; stringly typed errors;Resultignored withlet _ =. - Relearn: What can fail here, and who decides the policy?
Propagate with context; decide where you have enough information.
- Adopt: Use
?to pass errors upward, add context at boundaries, and handle errors where recovery is possible. - Smell:
?everywhere with no context; errors swallowed by a catch-all; handling policy buried in a low-level helper. - Relearn: Who is responsible for handling this error?
A caller should be able to match on what went wrong.
- Adopt: Design error enums with meaningful variants. Implement
Display,std::error::Error, andsource. Treat variants as semver-relevant. - Smell:
Other(String)as the only variant; breaking error variants in a patch release; errors with no source chain. - Relearn: Can a caller match on this error and act on it?
unwrapis a claim that failure is impossible.
- Adopt: Return an error or use
expectwith a reason only for a proven invariant. Test the failure path. - Smell:
unwrap()in library code;expect("should never happen")on user input; panics crossing an FFI boundary. - Relearn: Is this failure impossible, or just unlikely?
Absence and failure are different outcomes.
- Adopt: Use
Option<T>for “may be absent” andResult<T, E>for “may fail.” Do not collapse them into one type. - Smell:
Option<Result<T, E>>;Nonemeaning “failed”; error information lost in amap/and_thenchain. - Relearn: Is this absent, or did it fail?
A panic is a broken invariant, not a control-flow tool.
- Adopt: Use
assert!for preconditions,debug_assert!for internal invariants, andpanic!only for programmer errors. - Smell: Panicking on user input;
catch_unwindas error handling;unwrapin a request handler. - Relearn: Is this a bug or an expected failure?
“Failed” is not a diagnosis.
- Adopt: Add context at boundaries with
thiserrorsources oranyhow::Context. Preserve the source chain and include the identifiers needed to investigate. - Smell: An error that says only “failed”; the source chain discarded; context that repeats
Displaywithout adding information. - Relearn: What would I need to debug this at 3 a.m.?
An error the caller cannot act on is a log line with extra steps.
- Adopt: Distinguish retryable from permanent failures, expose typed reasons, and document the recovery path. Avoid collapsing everything into one opaque variant.
- Smell: Every error fatal; the caller cannot tell whether to retry; an error enum with a single
Othervariant. - Relearn: What can the caller do differently because of this error?
Rust gives you control; it does not remove the cost.
Allocation is a cost, not a reflex.
- Adopt: Keep small, fixed-size values on the stack. Box large or recursive values deliberately. Measure before assuming the heap is the problem.
- Smell:
Box<T>around everything; huge arrays on the stack; allocation in a hot loop because it was convenient. - Relearn: Does this value need heap allocation, and why?
Use
Boxto solve a shape problem, not to avoid thinking about ownership.
- Adopt: Use
Boxfor recursive types, large values, and trait objects. Use generics when static dispatch is enough; useRc/Arcfor sharing. - Smell:
Box<dyn Trait>where a generic would do; double indirection; boxing to satisfy a lifetime you could restructure. - Relearn: What problem is this indirection solving?
Every reallocation copies; reserve when you know the size.
- Adopt: Use
Vec::with_capacity, reuse buffers withclear(), and avoid collecting into temporaries in hot paths. - Smell: Repeated
pushwithout capacity;collect()just to iterate; allocating a newVecper request. - Relearn: How many allocations does this loop make?
Borrow when you can; own when you must.
- Adopt: Return
Cow<'a, str>orCow<'a, [T]>when a value is sometimes borrowed and sometimes owned. Keep the type simple. - Smell:
Coweverywhere as a complexity tax;Stringreturned for static data; cloning just to satisfy a return type. - Relearn: Does this value need ownership, or only sometimes?
Match the allocator to the lifetime.
- Adopt: Use a bump allocator such as
bumpalofor request-scoped or compiler-style data that is freed all at once. - Smell: An arena for long-lived data; lifetime complexity that outweighs the allocation savings; arenas crossing async boundaries without a plan.
- Relearn: What is the lifetime of this allocation, and can it be bulk-freed?
mem::forgetis safe; that does not make it free.
- Adopt: Use
Box::leak,mem::forget, andManuallyDroponly with a documented reason. Prefer owning values orArcover leaks. - Smell: Leaking to satisfy
'static; forgetting destructors; memory growth that “does not matter” until it does. - Relearn: What is leaked, how much, and for how long?
Avoid hidden allocations in APIs
A signature should not hide a heap trip.
- Adopt: Document when a function allocates. Accept caller-provided buffers, return iterators, and avoid temporary
VecorStringconstruction in hot paths. - Smell: “No allocation” claims with a hidden
collect; an API that allocates on every call; a clone inside a getter. - Relearn: How many allocations does one call make?
Capacity is an asset; do not throw it away.
- Adopt: Pool buffers,
clear()and reuse them, accept&mut Vec<u8>, and useBytesMutwhere shared slices are needed. - Smell: A new
Vecper request; capacity churn; allocating inside a loop that already has a buffer. - Relearn: What can be reused instead of reallocated?
unsafe does not mean “dangerous”; it means “the compiler cannot prove this.” You must.
Every
unsafeblock needs a reason the compiler cannot check.
- Adopt: Write a
// SAFETY:comment for every block: which invariant holds, why it holds, and what would break it. - Smell:
unsafewith no comment; “trust me”; copying unsafe code without understanding its preconditions. - Relearn: What invariant makes this sound, and what breaks it?
One small safe wrapper beats unsafe scattered everywhere.
- Adopt: Put
unsafebehind a safe abstraction, keep blocks small, and test the abstraction’s contract. - Smell:
unsafein business logic; unsafe blocks copied across modules; safe APIs that can still cause UB through misuse. - Relearn: Can this be safe outside one module?
The caller needs to know what you need them to guarantee.
- Adopt: Document aliasing, alignment, initialization, lifetime, and thread-safety requirements. Use
# Safetysections in rustdoc. - Smell: Safety docs that restate the code; missing preconditions;
unsafe fnwith no# Safety. - Relearn: What must the caller guarantee for this to be sound?
Across the boundary, Rust’s guarantees stop.
- Adopt: Define ownership, allocation, error, and panic behavior across FFI. Use
extern "C",#[no_mangle], and explicit conversion types. - Smell: Passing Rust types with no layout guarantee; unwinding across FFI; freeing memory in the wrong allocator.
- Relearn: Who allocates, who frees, and what happens on panic?
If another language reads it, the layout must be guaranteed.
- Adopt: Use
#[repr(C)]or#[repr(transparent)]at FFI boundaries. Checksize_of,align_of, and field order. Do not assumerepr(Rust)layout. - Smell: Transmuting structs; assuming field order; sending
boolor enums across FFI without a defined representation. - Relearn: Is this layout guaranteed by the language, or only by today’s compiler?
Unsafe code needs a tool that checks the rules you cannot see.
- Adopt: Run
cargo miri testfor UB, ASan/TSan for FFI and concurrency, and fuzz unsafe parsers. - Smell: Unsafe code tested only on the happy path; no Miri in CI; UB hidden behind passing tests.
- Relearn: Which tool would catch the class of UB I fear?
Pinning is a promise about address stability, not a smart pointer.
- Adopt: Treat
Pinas a guarantee that a value will not move. Usepin!orBox::pin, document structural pinning, and never move pinned data. - Smell:
Pinused as a buzzword; moving data out of a pinned value;Unpinassumptions that are not justified. - Relearn: What must never move, and who guarantees it?
A panic crossing the boundary is undefined behavior waiting to happen.
- Adopt: Catch panics at the boundary with
catch_unwind, or useextern "C-unwind"only when both sides support unwinding. Choose a panic strategy deliberately. - Smell: A panic crossing into C or C++; no panic strategy at the boundary;
catch_unwindwithout checking the result. - Relearn: What happens if this panics at the boundary?
Callers should not need to remember the C rules.
- Adopt: Keep raw pointers and
unsafeinside one module. Expose safe types that encode ownership and free resources inDrop. - Smell: Raw pointers in a public API; callers must remember to call
free; duplicate unsafe wrappers around the same library. - Relearn: Can a safe caller misuse this API?
Rust prevents data races, not deadlocks, starvation, or bad designs.
Auto traits encode what may cross a thread boundary.
- Adopt: Understand why a type is
SendorSync. UsePhantomDatato opt out. Neverunsafe impl Sendwithout a written proof. - Smell:
unsafe impl Sendto silence an error;Rcacross threads;Syncassumed because a type is immutable. - Relearn: What makes this type safe to send or share?
Send ownership; do not share mutation.
- Adopt: Use
std::sync::mpsc,crossbeam, orflumeto move messages. Keep mutable shared state small and explicit. - Smell: A global
Mutex<HashMap<...>>; lock contention; threads mutating the same structure. - Relearn: Can I send the data instead of sharing it?
Shared mutation is a design choice with a cost.
- Adopt: Use
Arc<Mutex<T>>when shared mutation is required,RwLockfor read-heavy access, and message passing when it is not. - Smell:
Arc<Mutex<Vec<T>>>everywhere; long critical sections; locking in async code without care. - Relearn: Who needs to mutate this, and how often?
Atomicity is not the same as visibility.
- Adopt: Choose
Relaxed,Acquire,Release, orSeqCstdeliberately. Document the ordering each access requires. - Smell:
SeqCsteverywhere “to be safe”;Relaxedused for publication; hand-rolled fences without a model. - Relearn: What ordering does this access require, and what breaks if I weaken it?
If the thread ends before the scope, it can borrow.
- Adopt: Use
std::thread::scopeto borrow stack data safely. AvoidArcclones and'staticbounds when the scope already proves the lifetime. - Smell: Cloning
Arcjust to spawn a short-lived thread;'staticeverywhere; detached threads that outlive their data. - Relearn: Does this thread need to outlive the current scope?
Use the right parallelism for the workload.
- Adopt: Use
rayonfor CPU-bound, divisible work. Keep I/O on async or dedicated threads. Measure before adding parallelism. - Smell: Rayon for I/O; nested parallelism causing oversubscription; parallelizing a workload that is already memory-bound.
- Relearn: Is this workload CPU-bound and divisible?
Deadlock is a design bug the compiler will not catch.
- Adopt: Establish a global lock order, document it, and keep critical sections small. Use
try_lockto diagnose rather than to paper over the design. - Smell: Two locks acquired in different orders; nested locks; a lock held across
.await. - Relearn: What is the lock order, and where is it written?
An unbounded queue is a memory leak with extra steps.
- Adopt: Prefer bounded channels, apply backpressure, and choose a reject-or-block policy. Monitor queue depth and latency.
- Smell: Unbounded channels; memory grows with a slow consumer; no drop or rejection policy.
- Relearn: What happens when the consumer is slower than the producer?
A poisoned lock means an invariant was already broken.
- Adopt: Handle
PoisonErrordeliberately: recover if the data is still valid, otherwise propagate. Avoidunwrap()on lock acquisition. - Smell:
lock().unwrap()everywhere; ignoring a poisoned state; panicking while holding a lock. - Relearn: What invariant was broken when the lock was poisoned?
Async is a scheduling model, not a magic performance switch.
Use async to wait on many things, not to compute faster.
- Adopt: Use async for network and disk waits. Use threads or
rayonfor CPU-bound work. Keep the distinction visible in the API. - Smell: Async for CPU-heavy loops; one task per core; async used because it is fashionable.
- Relearn: Is this workload waiting or computing?
A blocked task blocks every task on that worker.
- Adopt: Use
spawn_blockingor a dedicated thread for blocking I/O and CPU work. Keep tasks short between.awaitpoints. - Smell:
std::fsor blocking HTTP in async code; long CPU loops;Mutexheld across.await. - Relearn: What is this task preventing the executor from doing?
Dropping a future cancels it; cleanup must survive that.
- Adopt: Make futures cancel-safe. Use guards for cleanup and avoid partial writes that cannot be resumed.
- Smell: Cleanup only on success; partial state after cancellation;
select!dropping a future mid-write. - Relearn: What happens if this future is dropped mid-await?
Every await needs a bound, and every race needs a policy.
- Adopt: Wrap operations in
tokio::time::timeout. Useselect!for racing, and decide what happens to the losing branch. - Smell: No timeouts;
select!without anelsebranch; cancelled work that leaves state behind. - Relearn: What is the deadline, and what happens when it fires?
Every task has an owner and a shutdown path.
- Adopt: Use
JoinSetorTaskTrackerto own tasks. Await them on shutdown. Avoid detachedtokio::spawnunless the task is truly independent. - Smell: Fire-and-forget
tokio::spawn; tasks outliving the request; shutdown that waits forever. - Relearn: Who owns this task, and when is it joined?
A library should not force a runtime on its users.
- Adopt: Keep libraries runtime-agnostic where possible. Choose Tokio or async-std at the application edge, and document the choice.
- Smell: Tokio types in a public API; multiple runtimes in one process; runtime-specific behavior leaking into a generic crate.
- Relearn: Is this crate tied to a runtime, and should it be?
A future does nothing until someone polls it.
- Adopt: Await or spawn futures deliberately. Remember that constructing an
async fnfuture does not run its body. - Smell: A future created for its side effects and then dropped; an
async fncalled without.await; spawning only to start work that is never joined. - Relearn: Who polls this future, and when?
Accepting work is a promise to finish it.
- Adopt: Bound channels, use semaphores, apply load shedding, and propagate pressure to the caller. Decide what to reject before the queue grows.
- Smell: Unbounded
spawn; queues that grow under load; an accept loop with no limit. - Relearn: What happens when work arrives faster than it completes?
A detached task is ownership you gave away.
- Adopt: Keep the
JoinHandle, track tasks in a registry orJoinSet, and cancel or await them during shutdown. - Smell: Detached
tokio::spawn; no shutdown path; tasks that outlive the request that created them. - Relearn: Who waits for this task, and how is it stopped?
The optimizer is not a promise. The cost model is your responsibility.
An iterator chain is an abstraction over a loop, not a guarantee about the loop.
- Adopt: Prefer iterator chains for clarity, then check the generated code or benchmark when the path is hot. Watch for
Box<dyn Iterator>, closures that capture heavily, and chains that hide allocation. - Smell: “Iterators are always zero-cost” used to skip measurement;
dyn Iteratorin a tight loop; a chain that is rewritten by hand for speed without a profile. - Relearn: What machine code did this chain become, and did I look?
Collecting is a materialization decision, not punctuation.
- Adopt: Chain lazily and consume once. Collect only when you need random access, reuse, or a boundary. Use
collect::<Result<Vec<_>, _>>()deliberately. - Smell:
collect()followed immediately byiter(); collecting into aVecjust to call.len(); multiple passes over materialized data that could be one pass. - Relearn: Does this data need to exist all at once, or can it flow?
Safety has a cost, but often less than the folklore claims.
- Adopt: Write indexed code clearly first. If a bounds check shows up in a profile, restructure with iterators, slices, or
get_uncheckedbehind a proven invariant. - Smell:
unsafe { get_unchecked }before profiling; assuming every check is a bottleneck; removing checks without a// SAFETY:proof. - Relearn: Did the profiler blame the check, or did I?
Move bytes without copying them.
- Adopt: Accept
&[u8]andimpl AsRef<[u8]>for parsing. Usebytes::Bytesfor cheaply cloned, shared buffers in async and network code. AvoidVec<u8>copies at every layer. - Smell:
Vec<u8>cloned per handler;Stringfor binary data; converting between&strand&[u8]repeatedly. - Relearn: How many times are these bytes copied, and who owns them?
Vectorization is a portability decision as much as a performance one.
- Adopt: Let the compiler auto-vectorize clean loops first. Use
std::simd(nightly) orwide/std::archwhen measured. Gate target features with#[target_feature]and runtime detection. - Smell: Hand-written SIMD before a scalar baseline;
target_featurewithout a runtime check; code that only builds on one CPU. - Relearn: Which CPUs must run this, and how is the feature selected?
Intuition about performance is a hypothesis, not a measurement.
- Adopt: Use
perf,cargo flamegraph,criterion, andiai-callgrind. Establish a baseline, change one thing, and compare distributions, not single runs. - Smell: Optimizing the code that “looks slow”; micro-benchmarks with no realistic workload; claiming a win without a before-and-after.
- Relearn: What did the measurement say, and what did I change?
A latency win that allocates more is often a throughput loss.
- Adopt: Track allocations per operation with DHAT, heaptrack, or a counting allocator. Compare distributions and peak memory, not only wall time.
- Smell: A faster benchmark with an allocation regression; no allocation baseline; performance work that ignores memory.
- Relearn: What did this change cost in allocations and peak memory?
The fastest data is the data already in cache.
- Adopt: Choose layouts for locality: struct-of-arrays when scanning, compact enums, contiguous buffers. Measure cache misses and branch misses.
- Smell: Linked lists in a hot path;
Vec<Box<T>>for sequential data; random access over a large array. - Relearn: Where does this data live, and how far is it from the CPU?
On a small device, the platform is part of the design.
Every dependency on the platform must be deliberate.
- Adopt: Separate core logic from
std. Usecoreandallocexplicitly, and test on the target, not only on the host. - Smell: An accidental
stddependency; heap allocation assumed; host-only tests for target behavior. - Relearn: What does this code need from the platform?
On a microcontroller, a panic is a reset with extra steps.
- Adopt: Prefer
Resultin embedded paths, define a panic handler, and measure the code size and latency cost of failure. - Smell:
unwrap()in an interrupt handler; panics inno_std; unwinding on a target that cannot afford it. - Relearn: What happens when this panics on the target?
An interrupt is another thread you did not spawn.
- Adopt: Treat interrupt handlers as concurrent code. Use critical sections or atomics, keep handlers short, and defer work to the main loop.
- Smell: Shared state without synchronization; long interrupt handlers; blocking inside an interrupt.
- Relearn: What can preempt this code, and what does it touch?
While the hardware reads a buffer, the CPU must not touch it.
- Adopt: Model DMA buffers as owned or moved values. Enforce lifetime, alignment, and cache maintenance before and after transfer.
- Smell: CPU and DMA touching the same buffer; freeing a buffer during transfer; stale cache after DMA.
- Relearn: Who owns this buffer while the hardware reads it?
On a constrained target, the worst case is the only case that matters.
- Adopt: Prefer static allocation and pools. Budget stack usage, avoid unbounded recursion, and measure the worst-case footprint.
- Smell: Heap allocation in firmware without a need; unbounded recursion; stack overflow discovered in the field.
- Relearn: What is the worst-case memory use?
A missed deadline is a failure, not a slow success.
- Adopt: Bound worst-case execution time, jitter, and blocking. Test under load and interference. Keep unbounded operations out of the critical path.
- Smell: Average latency used to claim real-time; garbage collection or page faults in a control loop; priorities without worst-case analysis.
- Relearn: What is the worst case, and what happens when it is missed?
Every cycle awake has an energy cost.
- Adopt: Sleep when idle, batch work, avoid busy-waiting, and measure energy and thermal behavior. Design for throttling and battery limits.
- Smell: Spin loops; an always-on radio; performance claims that assume unlimited power.
- Relearn: What does this cost in energy, and when is it awake?
A public API is a contract the compiler cannot fully enforce.
Be liberal in what you accept and explicit in what you return.
- Adopt: Take
&str,&[T], andimpl AsRef<T>on the way in. ReturnString,Vec<T>, or an owned type when the caller must keep it. UseCowonly when the condition is real. - Smell:
&Stringand&Vec<T>parameters; returning references tied to internal state; forcing callers to clone before every call. - Relearn: Does the caller need to keep this value, or only look at it?
Static dispatch is fast and viral; dynamic dispatch is flexible and indirect.
- Adopt: Use generics for hot paths and small, known type sets. Use
dyn Traitfor heterogeneous collections, plugin boundaries, and reducing monomorphization. Measure compile time and binary size too. - Smell: Generic explosion across a large API;
dynin a tight numeric loop; choosing one without naming the tradeoff. - Relearn: Am I optimizing for runtime, compile time, or extensibility?
A trait is a role, not a bag of unrelated abilities.
- Adopt: Prefer several focused traits with blanket impls over one large trait. Use associated types for one-per-impl relationships and generic parameters for many-per-impl.
- Smell: God traits with twenty methods; default methods that assume implementation details; traits that exist only to group functions.
- Relearn: What single role does this trait describe?
Every public item is part of the contract, including error variants and trait impls.
- Adopt: Treat public types, trait methods, enum variants, and feature names as semver-relevant. Use
cargo-semver-checksin CI. Add variants only with care. - Smell: Breaking changes in a patch release;
#[non_exhaustive]added too late; “it’s just an internal type” that is actually public. - Relearn: What did I promise to downstream crates, and did this change break it?
Every feature combination is a crate you have not tested.
- Adopt: Keep features additive, avoid mutually exclusive flags, and test the combinations you claim to support. Document what each feature enables and its default.
- Smell: Features that change public types; default features pulling heavy dependencies; combinations that fail to compile.
- Relearn: Which feature combinations do I actually support, and where is that tested?
If the docs do not state the contract, the contract is whatever the code happens to do.
- Adopt: Write crate-level docs with examples,
# Errors,# Panics, and# Safetysections. Use doctests as executable examples. Document invariants and complexity. - Smell: Missing
# Safetyonunsafe fn; examples that no longer compile; docs that describe the implementation instead of the contract. - Relearn: Could a new user use this correctly without reading the source?
Many optional parameters are a type asking for a builder.
- Adopt: Use a builder for optional configuration, with a
build()that validates. Use typestate builders when some fields are mandatory. - Smell: An eight-argument constructor; boolean parameters; a public struct that can be constructed half-valid.
- Relearn: Can construction be guided step by step, with invalid combinations rejected?
The zero configuration is the configuration most users run.
- Adopt: Choose secure, unsurprising defaults. Make risky behavior explicit opt-in. Implement
Defaultonly when the default is obvious and valid. - Smell: An insecure default;
Defaultwith surprising behavior; a zero value that is invalid but constructible. - Relearn: What happens if the user does nothing?
Leave room to add a variant without breaking the world.
- Adopt: Put
#[non_exhaustive]on public enums and structs likely to grow. Document that downstream matches need a wildcard. - Smell: Adding a variant breaks downstream; exhaustive matches in user code; no migration path for new fields.
- Relearn: Can I add a variant or field without breaking callers?
A trait should enable genericity or extension, not decorate a type.
- Adopt: Start with inherent methods. Introduce a trait when you need a generic abstraction, a shared contract, or external extension.
- Smell: A trait with one implementation; trait imports required for basic use; method-resolution confusion from overlapping traits.
- Relearn: Is this trait enabling genericity, or just grouping functions?
Macros are powerful because they see syntax. That is also why they are expensive.
If a function, generic, or trait can express it, use that.
- Adopt: Reach for a macro only for syntax that cannot be expressed otherwise, or for code generation that removes real repetition.
- Smell: A macro for a two-line function; a macro hiding control flow; an expansion nobody can read.
- Relearn: What can a function or generic do that this macro cannot?
Match the macro kind to the job.
- Adopt: Use
macro_rules!for pattern-based repetition and simple syntax. Use procedural macros for derives, attributes, and generated items that need parsing. - Smell: A procedural macro for simple substitution;
macro_rules!parsing a complex grammar; a proc macro that reimplements a parser badly. - Relearn: Which kind of macro fits this job?
A macro must not accidentally capture the caller’s names.
- Adopt: Use
$crate, unique identifiers, and explicit paths. Avoid relying on names or imports the caller happens to have. - Smell: A macro breaks when the caller defines a same-named item; unqualified
Vec; reliance on caller imports. - Relearn: What names can this macro accidentally capture?
Generated code is still code someone has to maintain.
- Adopt: Inspect expansions with
cargo expand, keep them small, and point spans at the user’s input. Test the expansion where it matters. - Smell: Hundreds of lines from one macro; errors that point at macro internals; no one has ever run
cargo expand. - Relearn: Can a user understand the code this macro generates?
A derive should add the obvious impl and nothing surprising.
- Adopt: Keep derives predictable, document custom attributes, and avoid hidden behavior or side effects.
- Smell: A derive with side effects; surprising trait bounds; attributes that silently change semantics.
- Relearn: Would a user be surprised by what this derive does?
A macro is a language; document its grammar.
- Adopt: Document accepted forms, examples, and failure modes. Use doctests for macro examples and show the expansion when it helps.
- Smell: An undocumented DSL; examples only in tests; error messages nobody has explained.
- Relearn: Can someone use this macro from the docs alone?
A wire format is a promise to every future reader.
Deriving
Serializeis a design decision about your public data.
- Adopt: Define wire types separately from domain types when they diverge. Document field names, defaults, and compatibility rules. Test round trips against fixtures.
- Smell: Deriving
Serializeon internal types; the wire format coupled to struct layout; no schema or compatibility test. - Relearn: What have I promised to every reader of this format?
Data outlives the code that wrote it.
- Adopt: Include a version, prefer additive changes, define the unknown-field policy, and keep a migration path.
- Smell: A breaking field rename; no version marker; consumers break silently after a deploy.
- Relearn: How will next year’s reader handle today’s data?
Deserialization is where untrusted data becomes typed data.
- Adopt: Deserialize into validated types, reject invalid input early, and do not trust field presence or ranges.
- Smell:
Valuepassed deep into the system; validation repeated at every layer; defaults hiding missing data. - Relearn: What type proves this message was valid?
Dynamic JSON belongs at the edge, not in the domain.
- Adopt: Deserialize into typed structs at the boundary. Use
Valueonly for genuinely dynamic edges, and convert once. - Smell:
Valuethreaded through business logic; stringly field access; runtime type errors that the compiler could catch. - Relearn: Where does dynamic data become typed?
A parser error is not a user-facing diagnosis.
- Adopt: Map serde errors to domain errors with field paths and context. Do not leak parser internals or lose the location.
- Smell: Raw serde error returned to the user; no field path; a generic “invalid JSON” for every failure.
- Relearn: Can the caller tell which field failed?
Reading bytes in place is only sound if the layout allows it.
- Adopt: Use formats such as
rkyvor FlatBuffers with explicit alignment guarantees. Validate before access and document endianness. - Smell: Transmuting bytes into structs; unaligned access; assuming host endianness on the wire.
- Relearn: What guarantees this buffer can be read in place?
Strictness is a policy, not an accident.
- Adopt: Choose
deny_unknown_fieldsfor strict inputs and allow unknown fields for forward compatibility. Document the choice and test it. - Smell: Accidental strictness; silent data loss; changing the policy in a patch release.
- Relearn: What should happen when a new field arrives?
The network is a hostile, asynchronous, partially ordered place.
Assume loss, duplication, reordering, and partition.
- Adopt: Design for idempotency, retries, and partial failure. Make message handling safe when the same message arrives twice.
- Smell: Assuming ordered delivery; no retry or deduplication; a protocol that breaks on reconnect.
- Relearn: What happens if this message arrives twice or not at all?
An operation without a deadline can wait forever.
- Adopt: Set connect, read, write, idle, and total deadlines. Propagate them through the call stack and test that they fire.
- Smell: No timeout; infinite retry; a timeout only at the outermost edge.
- Relearn: What is the deadline, and who enforces it?
A retry storm is an outage that feeds itself.
- Adopt: Use exponential backoff with jitter, retry budgets, idempotency keys, and a capped attempt count.
- Smell: Immediate retry loops; synchronized retries across clients; retrying non-idempotent writes.
- Relearn: Could this retry make the outage worse?
A stream has no message boundaries until you define them.
- Adopt: Use length-prefixed or delimiter framing with a maximum frame size. Handle partial reads and writes correctly.
- Smell: Assuming one read equals one message; no maximum size; delimiter injection.
- Relearn: Where does one message end and the next begin?
A fast writer and a slow reader are a memory leak in progress.
- Adopt: Use bounded buffers, flow control, and load shedding. Propagate pressure instead of absorbing it silently.
- Smell: An unbounded read buffer; a slow consumer with no policy; an accept loop without a limit.
- Relearn: What happens when the peer reads slower than I write?
Plaintext across a trust boundary is a data breach waiting for a network tap.
- Adopt: Use
rustlsor platform TLS, verify certificates, set SNI correctly, and plan rotation. Test expiry and verification failures. - Smell: Plaintext across a trust boundary; disabled certificate verification; a pinned certificate with no rotation plan.
- Relearn: Who can read or modify this traffic?
A connection is a resource with a lifecycle, not a boolean.
- Adopt: Bound the pool, health-check connections, apply acquisition and idle timeouts, and discard broken connections.
- Smell: An unbounded pool; stale connections reused after failure; pool exhaustion under load.
- Relearn: What happens when every connection is busy or dead?
Security is a property of the whole system, not a crate you add.
You cannot defend an asset you have not named.
- Adopt: Identify assets, actors, trust boundaries, and attack surface. Write the model down and revisit it when the system changes.
- Smell: Security added at the end; no adversary model; “we’re internal, so it’s fine.”
- Relearn: Who would attack this, and what do they gain?
Cryptography is the one place where “I understand the math” is not enough.
- Adopt: Use vetted crates such as
ring, RustCrypto, orsodiumoxide. Prefer high-level APIs and never invent a protocol. - Smell: A custom cipher; a hand-rolled nonce; “simple” crypto; a DIY key exchange.
- Relearn: Who has reviewed this construction, and what standard is it based on?
A
Stringcan be cloned, logged, and swapped.
- Adopt: Use
secrecyandzeroize, avoidDebug/Displayon secrets, limit copies, and considermlockfor high-value keys. - Smell: Secrets in logs;
Stringsecrets; cloned secrets; secrets in core dumps. - Relearn: Where can this secret be copied, logged, or swapped?
Every boundary is an input validation problem.
- Adopt: Parse into constrained types, enforce bounds and canonicalization, and reject by default. Treat deserialization as hostile input.
- Smell: Trusting lengths; path traversal; deserializing untrusted data into powerful types; shell interpolation.
- Relearn: What is the worst input an attacker can send?
A timing difference can be a side channel.
- Adopt: Use
subtlefor secret-dependent comparisons, avoid secret-dependent branches, and measure where the threat model requires it. - Smell:
==on MACs; an early return on a secret byte; timing leaks in token comparison. - Relearn: Does the time this takes reveal the secret?
Every crate is code you ship and trust.
- Adopt: Audit, minimize, and pin dependencies. Review build scripts, use
cargo-vetorcargo-deny, and monitor advisories. - Smell: Unmaintained crates; unknown build scripts; typosquatting; no audit in CI.
- Relearn: What code am I trusting that I did not write?
A process should have only the access it needs.
- Adopt: Drop privileges, sandbox where possible, apply seccomp or capabilities, and run with a minimal filesystem view.
- Smell: Running as root; broad filesystem access; unnecessary capabilities; one process for everything.
- Relearn: What is the minimum this process needs?
Logs are a copy of your data with a different retention policy.
- Adopt: Redact secrets and PII, use structured fields, set retention, and restrict access to logs.
- Smell: Tokens in logs; full request bodies; unbounded retention; logs shipped to a third party unchecked.
- Relearn: What would an attacker learn from my logs?
Tests are evidence, not decoration.
Test the unit where it is defined, including private behavior.
- Adopt: Use
#[cfg(test)] mod testsbeside the code. Test invariants, edge cases, and error paths, not just the happy path. - Smell: Tests only in a distant integration suite; no test for the error branch; tests that assert implementation details.
- Relearn: Which invariant does this test prove, and where does it live?
Test the API the user actually sees.
- Adopt: Put tests in
tests/and exercise the public API. Usetrybuildfor compile-fail tests and doc tests for examples. - Smell: Only unit tests, never a public-API test; integration tests reaching into private modules; examples that are not tested.
- Relearn: Does this test use the crate the way a caller would?
Example tests check the cases you thought of; properties check the ones you did not.
- Adopt: Use
proptestorquickcheckfor invariants, andcargo-fuzz/libFuzzerfor parsers and unsafe boundaries. Keep failing cases as regression tests. - Smell: Only hand-picked inputs; fuzzing without a corpus; property tests that restate the implementation.
- Relearn: What property must hold for all inputs, and what generated the counterexample?
Some bugs are invisible to ordinary tests.
- Adopt: Run
cargo miri testfor undefined behavior,loomfor concurrency models, and ASan/TSan for FFI and threaded code. Put them in CI where they matter. - Smell: Unsafe code tested only on one platform; concurrency “verified” by a passing test run; sanitizers run once and never again.
- Relearn: Which tool would catch the bug class I cannot see?
Debugging is a skill of asking the system what it did.
- Adopt: Use
dbg!locally,tracingfor structured events, andRUST_BACKTRACE=1withpanic = "abort"considered. Keep spans across async boundaries. - Smell:
println!debugging left in production; logs without request IDs; backtraces disabled in release with no alternative. - Relearn: What evidence would tell me what actually happened?
A bug you cannot reproduce is a bug you do not yet understand.
- Adopt: Capture the exact input, environment, and version. Reduce to the smallest failing case. Use
git bisectandcargo bisect-rustcwhen the compiler or dependency changed. - Smell: Fixing by guesswork; “cannot reproduce” closing the issue; changing several things at once.
- Relearn: What is the smallest input that still fails?
The happy path is the part you already know works.
- Adopt: Test errors, timeouts, cancellation, partial failure, and invalid input. Make mocks fail on purpose and assert the recovery behavior.
- Smell: Only success cases; an error branch with no test; a mock that never fails.
- Relearn: Which failure would hurt most in production, and where is its test?
A parser is defined by its corpus.
- Adopt: Keep snapshot inputs and outputs, review diffs deliberately, and include malformed and adversarial cases.
- Smell: Parser tests with no corpus; snapshot churn accepted blindly; no regression file for past bugs.
- Relearn: What input would break this parser, and is it in the corpus?
Some guarantees are enforced at compile time; test those too.
- Adopt: Use
trybuildto assert that invalid usage fails, auto traits behave, and error messages stay useful. - Smell: Safety claims with no compile-fail test; an API that compiles when it should not; error messages nobody checks.
- Relearn: What should fail to compile, and is that tested?
The build is part of the program.
If it is not in
Cargo.toml, it is not reproducible.
- Adopt: Keep dependencies, features, profiles, and metadata explicit. Commit
Cargo.lockfor binaries, use workspaces for multi-crate repos, and document build requirements. - Smell: Hand-edited build scripts that bypass Cargo; floating dependency versions; a lockfile that is never updated or always churning.
- Relearn: Can a new machine build this from the repository alone?
Style is not a debate; it is a baseline.
- Adopt: Run
cargo fmt --checkandcargo clippy -- -D warningsin CI. Fix lints or allow them with a reason. Keep the toolchain pinned. - Smell: Formatting-only diffs; dozens of ignored lints;
#[allow(clippy::all)]at the crate root. - Relearn: Which lint is telling me something about the design?
Every dependency is code you ship and trust.
- Adopt: Pin versions in
Cargo.lock, runcargo auditandcargo deny, and review new dependencies for license, maintenance, and unsafe use. - Smell: Dependencies added for one function; no audit in CI; transitive crates nobody has reviewed.
- Relearn: What does this dependency bring, and who maintains it?
A minimum supported Rust version is a compatibility contract.
- Adopt: Declare
rust-versioninCargo.toml, test it in CI, and treat raising it as a semver-relevant change. Avoid newer syntax until the MSRV allows it. - Smell: MSRV discovered by users; raising it in a patch release; CI testing only stable.
- Relearn: What is the oldest compiler this crate promises to support?
The artifact should be the same tomorrow as it was today.
- Adopt: Pin toolchains with
rust-toolchain.toml, vendor or lock dependencies, and verify checksums. Considercargo-vet, signed releases, and provenance. - Smell: Builds that differ between machines; unpinned nightly; no record of what produced a release binary.
- Relearn: Could I rebuild this exact artifact and prove it matches?
The best dependency is the one you did not add.
- Adopt: Prefer the standard library and small, focused crates. Check
cargo treeand binary size. Replace a heavy dependency with a few lines when the maintenance cost is lower. - Smell: A framework for one feature; duplicate crates doing the same job; compile times dominated by transitive dependencies.
- Relearn: What is the smallest dependency set that solves this problem?
A workspace is a statement about which crates change together.
- Adopt: Split crates when they have independent APIs, lifecycles, or compile costs. Share workspace dependencies and keep dependency edges acyclic.
- Smell: One giant crate; duplicated dependency versions across members; cyclic dev-dependencies.
- Relearn: What boundary does this workspace member represent?
If CI does not check it, it is a hope.
- Adopt: Run formatting, clippy, tests, MSRV, audit, docs, and the feature matrix. Fail fast, cache deliberately, and keep CI reproducible.
- Smell: CI that only builds; warnings allowed to accumulate; feature combinations untested; a green build that proves little.
- Relearn: What does CI prove, and what does it not?
Optimization settings change behavior, size, and debuggability.
- Adopt: Choose
opt-level, LTO,codegen-units, and panic strategy deliberately. Measure speed, binary size, and build time. - Smell: Default release settings for a latency-critical service;
panic = "abort"without understanding the tradeoff; LTO as cargo cult. - Relearn: What does this profile optimize for, and what does it cost?
If you cannot see it, you cannot operate it.
A log line is data, not a sentence.
- Adopt: Use
tracingwith fields, spans, and stable event names. Include request IDs and outcome, and choose levels deliberately. - Smell:
println!in production; concatenated strings; inconsistent field names; logs that cannot be queried. - Relearn: Can I query this log by request, user, and outcome?
A metric nobody queries is a cost with no benefit.
- Adopt: Instrument RED and USE metrics, use histograms for latency, keep label cardinality bounded, and tie metrics to SLOs.
- Smell: High-cardinality labels; averages only; dashboards nobody uses; metrics with no owner.
- Relearn: Which question does this metric answer?
A request is a story that crosses process boundaries.
- Adopt: Propagate trace context, create spans at meaningful boundaries, sample deliberately, and link logs and metrics to traces.
- Smell: Broken trace context; spans without attributes; 100% sampling in production; traces nobody can follow.
- Relearn: Can I follow one request across services?
Liveness and readiness answer different questions.
- Adopt: Keep checks cheap and bounded. Readiness reflects the ability to serve; liveness reflects whether a restart would help. Test the failure behavior.
- Smell: Readiness that checks every dependency; liveness that restarts on a dependency failure; expensive health checks.
- Relearn: What should happen when this check fails?
Processes stop; in-flight work should not disappear with them.
- Adopt: Handle signals, stop accepting, drain with a deadline, then force exit. Test shutdown under load.
- Smell: Only
SIGKILL; dropped in-flight requests; shutdown that hangs forever; no drain period. - Relearn: What happens to in-flight work when this process stops?
Configuration is untrusted data with a different delivery mechanism.
- Adopt: Parse into typed configuration at startup, validate ranges, document defaults, and fail fast on malformed input.
- Smell: Unchecked environment variables; configuration read everywhere; silent fallback; a zero value that means “unset.”
- Relearn: What happens with a missing or malformed setting?
Every resource is finite, including the ones you forgot to name.
- Adopt: Bound memory, file descriptors, threads, and connections. Set limits, handle exhaustion, and alert before the cliff.
- Smell: Unbounded caches; file descriptor leaks; one thread per request; no limit on concurrent work.
- Relearn: What happens when this resource runs out?
An incident is the wrong time to discover the procedure.
- Adopt: Document detection, mitigation, rollback, and escalation. Test the runbook in game days and keep it next to the service.
- Smell: Tribal knowledge; an outdated runbook; no rollback path; escalation that depends on one person.
- Relearn: Can someone on call act without me?
Code is read, changed, and deleted far more often than it is written.
A module should hide something, not just group files.
- Adopt: Organize by domain, keep the public surface small, and choose a consistent file layout. Let the module tree express ownership and visibility.
- Smell: A god module; circular dependencies; everything
pub; modules named after file types instead of concepts. - Relearn: What does this module hide from the rest of the crate?
Private by default; public on purpose.
- Adopt: Start private, use
pub(crate)for internal sharing, and expose only the API that needs a contract. Review everypubas a semver decision. - Smell:
pubeverywhere; internal types leaking; accidental public API; visibility widened to fix a test. - Relearn: Who actually needs to see this?
The crate root is a facade, not a dumping ground.
- Adopt: Use
lib.rsto declare modules and re-export the intentional API. Keep implementation in focused modules. - Smell: A thousand-line
lib.rs; business logic in the facade; an export list nobody can explain. - Relearn: Can I understand the crate’s surface from
lib.rs?
Split when the contract is independent, not because the file is long.
- Adopt: Create a crate when a component has its own lifecycle, API, or compile cost. Avoid premature splitting that adds versioning overhead.
- Smell: One crate per file; cyclic workspace dependencies; shared mutable internals across crates.
- Relearn: What independent contract does this crate own?
A removal is a breaking change; a deprecation is a migration path.
- Adopt: Mark items
#[deprecated]with a replacement and a migration window. Remove only in a major release, and document the path. - Smell: Silent removal; deprecation without an alternative; items deprecated forever with no plan.
- Relearn: How does a user migrate off this?
A refactor that does not compile is a rewrite.
- Adopt: Keep the tree green, separate mechanical changes from behavior changes, and rely on tests to preserve behavior.
- Smell: A giant refactor branch; renaming and behavior changes in one commit; no tests to catch regressions.
- Relearn: What is the smallest step that compiles and preserves behavior?
The safest code is the code that no longer exists.
- Adopt: Remove dead code, unused features, and stale docs. Use the compiler, coverage, and
cargo udepsto find candidates. - Smell: Commented-out code; dead feature flags; “we might need it later”; code kept only because deleting feels risky.
- Relearn: What breaks if I delete this?
Grouped by domain. Use the domain links above to jump to the full entries.
- Ownership and Borrowing: Borrow, don’t clone · Move is a feature · One owner at a time · Split borrows instead of fighting the checker · Interior mutability is a deliberate escape hatch ·
Dropis part of the contract · Shared ownership is a last resort - Lifetimes and References: Lifetimes describe, not create · Prefer owned data at boundaries · Do not outlive your borrow ·
'staticis not “forever” · Variance matters · Self-referential types need a plan · Borrow from the narrowest scope · Reborrow instead of moving - Types and Invariants: Make illegal states unrepresentable · Newtype everything meaningful · Enums over boolean flags · Parse, don’t validate · Typestate for protocols · Sealed traits for controlled extension · Const generics for compile-time shape ·
PhantomDatafor type-level meaning · Zero-sized types carry proof - Errors and
Option:Resultis the default ·?is for propagation, not policy · Error types are API · Do notunwrapin libraries ·Optionis not an error · Panics are for bugs · Errors should carry context · Design for the caller’s recovery - Memory and Allocation: Stack by default, heap by necessity ·
Boxis for indirection, not everything ·Veccapacity is a performance decision ·Cowis for conditional ownership · Arenas for phase-scoped data · Leaks are safe, rarely acceptable · Avoid hidden allocations in APIs · Reuse buffers across requests - Unsafe and FFI:
unsafeis a proof obligation · Minimize the unsafe surface · Document safety invariants · FFI boundaries are contracts ·repr(C)and layout · Miri and sanitizers ·Pinis a safety contract · Never unwind across FFI · Wrap C in a safe Rust API - Concurrency:
SendandSyncare contracts · Prefer channels to shared state ·Arc<Mutex<T>>is not a default · Atomics need ordering · Scoped threads beat'staticclones · Rayon for data parallelism · Avoid lock ordering inversions · Bound your queues · Poisoning is information - Async and Runtimes: Async is for I/O concurrency · Do not block the executor · Cancellation is a drop ·
select!and timeouts · Structured concurrency withJoinSet· Runtime choice is an API decision · Futures are lazy · Backpressure belongs in the design · Spawn only when you can join - Performance and Iterators: Iterators are zero-cost until they aren’t · Avoid needless
collect· Bounds checks are usually cheap; measure ·&[u8]andBytesfor I/O · SIMD and target features · Profile before optimizing · Measure allocations, not just time · Optimize the memory hierarchy - Embedded and Systems Constraints:
no_stdis a discipline · Panics are expensive · Interrupts are concurrency · DMA and ownership · Memory is fixed; plan it · Real-time is a deadline · Power is a design budget - API and Trait Design: Accept borrowed, return owned · Generics for zero-cost,
dynfor flexibility · Traits should be small and composable · Semver is a promise · Feature flags are a compatibility surface · Documentation is part of the API · Builders for complex construction · Defaults should be safe · Non-exhaustive for future growth · Prefer inherent methods until a trait is needed - Macros and Metaprogramming: Macros are a last resort · Declarative macros for syntax, procedural for structure · Hygiene is not optional · Keep macro expansion debuggable · Derive macros should be boring · Document macro syntax
- Serialization and Data Modeling: Serde is a schema, not a convenience · Version your wire formats · Validate at the boundary · Avoid
serde_json::Valuein core logic · Own your serialization errors · Zero-copy formats need alignment · Unknown fields are a compatibility decision - Networking and I/O: Treat the network as unreliable · Timeouts are mandatory · Retries need jitter and budgets · Framing is part of the protocol · Backpressure at every boundary · TLS is not optional · Connection pools are state machines
- Security and Cryptography: Threat model before code · Do not roll your own crypto · Secrets are not strings · Validate untrusted input · Constant-time where it matters · Dependencies are attack surface · Least privilege for processes · Log without leaking
- Testing, Debugging, and Verification: Unit tests next to code · Integration tests at crate boundary · Property tests and fuzzing · Miri, loom, and sanitizers ·
dbg!,tracing, and backtraces · Reproduce, reduce, bisect · Test the failure path · Golden tests for parsers · Compile-fail tests for APIs - Tooling, Build, and Ecosystem: Cargo is the build contract · Clippy and rustfmt are defaults · Pin and audit dependencies · MSRV is a promise · Reproducible builds and supply chain · Keep the dependency graph small · Workspaces for boundaries · CI is the executable contract · Release profiles are product decisions
- Observability and Operations: Structured logs, not printf · Metrics answer questions · Traces connect cause and effect · Health checks are contracts · Graceful shutdown is a feature · Configuration is input · Resource limits are part of design · Runbooks are code for humans
- Project Structure and Maintenance: Modules are boundaries · Visibility is a tool · Keep
lib.rssmall · Crate boundaries follow ownership · Deprecate before removing · Refactor in small, compiling steps · Delete code
Answer each line honestly. “Not sure” counts as no. Close the gaps you marked.
- I can explain who owns the data at every public API boundary.
- I have removed a
.clone()in the last month for a design reason. - My public types make invalid states hard to construct.
- Every fallible function returns
Result, and callers can match on errors. - I do not
unwrap()in library or request-handling code. - Every
unsafeblock has a// SAFETY:comment that names the invariant. - I know which
Send/Syncimpls are unsafe, and why they are sound. - I can name the ordering on every atomic access I wrote.
- No async task blocks the executor on file, network, or CPU work.
- I know what happens to each future when it is dropped.
- I have profiled the hot path instead of guessing.
- I know the allocation count per request on the hot path.
- My crate’s public API has documented errors, panics, and safety contracts.
- My wire formats have a version and a compatibility test.
- Every network call has a deadline, and retries have jitter and a budget.
- I know which inputs are untrusted, and they are parsed into constrained types.
- Secrets are redacted from logs and not stored as ordinary strings.
- My CI runs formatting, clippy, tests, MSRV, audit, and a feature matrix.
- I run Miri, loom, or a sanitizer on the code that needs it.
- I know my MSRV and test it.
- I can rebuild the last release artifact from the repository.
- I can follow one request across logs, metrics, and traces.
- I can explain what happens to in-flight work during shutdown.
- I can explain why the weirdest piece of code in my area exists.
Use this when reviewing a Rust pull request. It is a checklist of questions, not a style guide.
| Domain | Ask |
|---|---|
| Ownership | Who owns this value, and is the clone justified? |
| Lifetimes | What keeps this reference alive, and does the API force a lifetime on callers? |
| Types | Can an invalid state be constructed? Is the meaning in the type? |
| Errors | Can the caller distinguish absence from failure and act on it? |
| Memory | How many allocations per call, and what is their lifetime? |
| Unsafe | What invariant makes this sound, and what tests it? |
| Concurrency | What is shared, what is sent, and what ordering is required? |
| Async | What blocks, what is cancelled, and who owns the task? |
| Performance | What did the profiler say, and what does the abstraction cost? |
| Embedded | What does the platform not provide, and what is the worst case? |
| API | What is promised to downstream crates, and is it semver-safe? |
| Macros | Is this syntax or code generation, and can a user debug the expansion? |
| Serialization | What have I promised to every reader of this format? |
| Networking | What happens when the peer misbehaves or disappears? |
| Security | Who can reach it, what do they gain, and what stops them? |
| Verification | Which failure path is tested, and would a mutant survive? |
| Build | Is the build pinned, linted, audited, and reproducible? |
| Operations | Can I see what it is doing, and can someone else run it? |
| Maintenance | Can this be changed or deleted without a rewrite? |
A useful review comment names the mentality and the concrete risk:
“This
Arc<Mutex<Vec<_>>>is shared mutation by default. Only the metrics task writes it; can we send updates over a channel instead?”
The fastest way to lose a good Rust habit is to keep a comforting one nearby.
| Anti-pattern | What it sounds like | Replacement |
|---|---|---|
| Clone to compile | “Just add .clone().” |
“Who should own this, and why is borrowing hard?” |
Rc<RefCell<T>> by default |
“It works.” | “Is shared mutation actually required?” |
| Boolean state soup | “We check the flags.” | “Model the states as an enum.” |
| Stringly typed errors | “It returned an error.” | “Can the caller match on the failure?” |
unwrap() in production |
“It can’t fail.” | “Prove it, or return an error.” |
| Unsafe without a proof | “Trust me.” | “Write the // SAFETY: invariant.” |
Arc<Mutex<T>> everywhere |
“Shared state is simple.” | “Send ownership instead of sharing mutation.” |
| Blocking the executor | “It’s just one file read.” | “Use spawn_blocking or a dedicated thread.” |
| Fire-and-forget tasks | “It runs in the background.” | “Who joins it on shutdown?” |
| Collect-then-iterate | “It’s clearer.” | “Does this data need to exist all at once?” |
| Optimize by intuition | “This looks slow.” | “What did the profiler measure?” |
| Semver optimism | “It’s only a small change.” | “What did I promise downstream?” |
| Dependency reflex | “There’s a crate for that.” | “What does it bring, and who maintains it?” |
| Macro-first design | “A macro makes it concise.” | “Can a function or generic express this?” |
| Schema by accident | “Just derive Serialize.” |
“What wire format did I just publish?” |
| Timeout-free networking | “It usually responds.” | “What is the deadline, and who enforces it?” |
| Retry storm | “Retry until it works.” | “Where is the jitter, budget, and idempotency?” |
| Secrets as strings | “It’s just a token.” | “Where can this be logged or cloned?” |
println! observability |
“The logs are in stdout.” | “Can I query this by request and outcome?” |
| Permanent temporary code | “We’ll clean it up later.” | “What breaks if I delete it now?” |
Pick the format that fits your team, not the one that feels most impressive.
- 15-minute brown bag: one mentality, one Rust example from your own crate, one question at the end.
- Review comments: cite the entry by name. “This is Do not block the executor” is faster than a paragraph and easier to remember.
- Onboarding: choose the mentalities that matter most for your codebase and make them required reading.
- Postmortems: map contributing factors to missing mentalities, then add the missing test or lint.
- Lunch-and-learn series: one domain per week, cycling through the stack, then repeat with new examples.
One cluster per week. Each week ends with an artifact, not a feeling.
- Read Ownership and Borrowing, Lifetimes and References, Types and Invariants, Errors and
Option, and Memory and Allocation. - Pick one public API and one error type. Remove an unjustified clone, make an invalid state unrepresentable, and add an error test.
- Artifact: a diff that makes ownership or failure clearer.
- Read Unsafe and FFI, Concurrency, Async and Runtimes, Performance and Iterators, and Embedded and Systems Constraints.
- Pick one
unsafeblock, one shared-state path, and one hot path. Add a safety proof, bound a queue, and profile the hot path. - Artifact: a documented safety proof or a measured performance change.
- Read API and Trait Design, Macros and Metaprogramming, Serialization and Data Modeling, Networking and I/O, and Security and Cryptography.
- Pick one public trait, one wire format, and one network call. Check semver, add a compatibility test, and enforce a deadline.
- Artifact: an API decision record plus one boundary test.
- Read Testing, Debugging, and Verification, Tooling, Build, and Ecosystem, Observability and Operations, and Project Structure and Maintenance.
- Pick one untested failure path, one build promise, and one production signal. Add a property or fuzz test, verify the MSRV, and make one operation queryable.
- Artifact: a CI or observability change that would have caught a real problem.
Language and ownership
- The Rust Programming Language, Klabnik and Nichols — the canonical starting point.
- Programming Rust, Blandy, Orendorff, and Tindall — systems-oriented coverage of ownership and performance.
- Rust for Rustaceans, Jon Gjengset — idiomatic design, traits, async, and unsafe.
- Effective Rust, David Drysdale — focused advice with concrete examples.
- Learn Rust With Entirely Too Many Linked Lists — ownership and unsafe through deliberate exercises.
Unsafe, concurrency, and async
- The Rustonomicon — the unsafe code guidelines every
unsafeauthor should read. - Rust Atomics and Locks, Mara Bos — memory ordering and concurrent data structures.
- Async Rust, Maxwell Flitton and Caroline Morton — futures, runtimes, and practical async design.
- The Miri and loom documentation — executable checks for UB and concurrency.
Performance and systems
- The Rust Performance Book, Nicholas Nethercote — profiling, allocation, and optimization.
- The Embedded Rust Book —
no_std, HALs, interrupts, and target workflows. - Making Embedded Systems, Elecia White — firmware, hardware constraints, and practical embedded design.
API design, networking, and security
- Rust API Guidelines — naming, traits, documentation, and semver.
- Zero to Production in Rust, Luca Palmieri — building and operating a real service.
- The Cargo Book, the Rust Reference, and
cargo-semver-checksdocumentation. - The RustSec advisory database and
cargo-auditdocumentation. - TCP/IP Illustrated, W. Richard Stevens — the network behavior your abstractions assume.
Operations and learning
- Site Reliability Engineering, Beyer et al. — SLOs, error budgets, and operational design.
- Incident Management for Operations, Rob Schnepp et al. — command, communication, and coordination under pressure.
- Accelerate, Forsgren, Humble, and Kim — delivery performance as a system property.
- Keep entries Rust-specific: name the type, crate, lint, or tool.
- Keep the four-part shape: mantra, Adopt, Smell, Relearn.
- Prefer one concrete practice over three vague ones.
- Add entries to the domain that owns the decision, and update the index.
- If you add a domain, update the contents, the domain map, the review lens, and the four-week plan.
- Do not number entries. Names are the stable reference.