Rust Design Patterns
Design patterns in Rust are not a translation of the Gang of Four catalog into a new syntax.
They are a different answer to the same underlying question: how do you organize code so that correct usage is easy and incorrect usage is hard.
In languages built around classes and inheritance, that answer usually involves subtype polymorphism, virtual dispatch, and runtime checks.
In Rust, ownership, move semantics, and a strict compile-time type checker are already doing structural work for you, so the patterns that earn their keep are the ones that lean into that machinery instead of working around it.
This page is the conceptual anchor for the rest of the rust-patterns section.
It explains why patterns like the newtype wrapper, the builder, the typestate machine, and RAII guards look the way they do, and gives you a framework for deciding when each one is worth its added complexity.
Summary
- Rust design patterns push runtime invariants (valid IDs, valid state transitions, released resources) into the compiler, using ownership and the type system instead of inheritance or runtime checks.
- Insight: Bugs caught at compile time never reach production, and Rust's ownership model makes several categories of bug (use-after-free, forgotten cleanup, mixed-up identifiers) statically checkable in a way that garbage-collected, class-based languages cannot easily replicate.
- Key Concepts: nominal typing, zero-cost abstraction, ownership and move semantics, the typestate pattern, RAII (Resource Acquisition Is Initialization), illegal states unrepresentable.
- When to Use: Reach for these patterns when a bug class recurs across a codebase (mixed-up IDs, half-configured structs, resources leaked on an early return, methods called in the wrong order).
- Limitations/Trade-offs: Every pattern here trades some ergonomics or compile-time flexibility for the guarantee it buys, and overusing any of them adds ceremony without a matching payoff.
- Related Topics: ownership and borrowing, the trait system, error handling with
Result, API design in Rust.
Foundations
A "pattern" in the object-oriented sense is a named, reusable solution to a recurring design problem, and the original Gang of Four catalog assumes a language with classes, inheritance, and null references.
Rust has none of those three things in the form C++ or Java programmers expect.
There is no implementation inheritance, so patterns like Template Method or classic Decorator do not map over directly, and there is no null, so patterns built around defending against it are simply unnecessary.
What Rust does have is an unusually expressive type system, a compiler that tracks who owns each value and for how long, and a trait system for shared behavior without shared implementation.
Rust design patterns are what happens when you ask "how do I get this guarantee" using only those tools, and the four patterns this section centers on each answer a specific, common question with a specific compiler-enforced guarantee.
The newtype pattern wraps an existing type (often a primitive like u64 or String) in a one-field struct to create a new, unrelated type at the compiler's level.
struct UserId(u64);
struct OrderId(u64);
// a UserId can never be passed where an OrderId is expected,
// even though both are just a u64 underneathThis is nominal typing: two types with identical layout are still incompatible if they have different names, and Rust enforces that at zero runtime cost because the wrapper disappears after compilation.
The builder pattern separates the act of constructing a value from the value itself, which matters in a language with no default arguments or telescoping constructors.
The typestate pattern encodes an object's state (open, closed, draft, submitted) as a type parameter rather than a field, so the compiler refuses to compile a call that is only valid in a different state.
RAII ties resource cleanup to a value's lifetime: when a value that holds a lock, file handle, or connection goes out of scope, Rust's Drop trait runs automatically, with no garbage collector and no finally block required.
Mechanics & Interactions
These four patterns are not independent tricks.
They are four expressions of one mechanism: Rust's type checker treats every distinct type as a compile-time fact, and every value's lifetime as a scope the compiler can reason about statically.
Newtype is the simplest case: it creates a new fact ("this u64 is specifically a UserId") checked at every call site for free, because the wrapper is erased at compile time and the generated code matches the raw u64 version exactly.
Typestate generalizes this from "this value has a meaning" to "this value only permits certain method calls," by making the state part of the type itself, usually via a zero-sized marker type and PhantomData.
Because Rust methods are implemented per concrete type, a method defined only on Connection<Open> simply does not exist on Connection<Closed>, so calling it is a compile error instead of a runtime panic or a forgotten if check.
Typestate transitions almost always consume the old value (self, not &self) and return a new one, mirroring move semantics: once .connect() has been called, the old Connection<Disconnected> no longer exists, so there is no stale handle to accidentally reuse.
Builder uses this same move-based mechanic more gently.
Each setter takes self by value and returns self, so a chain like Client::builder().timeout(..).retries(..).build() is really a sequence of moves rather than mutations of a shared object.
RAII sits one layer below the other three, because it is the mechanism that makes ownership trustworthy for resource management in the first place.
A struct's Drop implementation runs deterministically the moment its owning scope ends, in the reverse order fields were declared, which is what lets guard types (a MutexGuard, a temp-file guard, a metrics timer) clean up correctly on every exit path, including early returns and, in an unwinding build, panics.
enter scope
acquire resource -> construct Guard
... do work, maybe return early, maybe panic ...
exit scope (any path)
Guard::drop() runs automatically -> resource releasedA common reasoning pitfall is expecting these guarantees to survive async or FFI boundaries unmodified.
Drop has no async variant, so cleanup requiring an .await cannot run automatically when a future is dropped, and typestate markers usually need erasing to a plain enum at an FFI or serialization boundary, since foreign callers and serde do not understand zero-sized phantom types.
Advanced Considerations & Applications
The real skill is not knowing the mechanics; it is judging when the guarantee is worth the ceremony, since every one of these patterns adds a layer a reader has to learn.
Newtype wrapper fatigue is a real cost: wrapping every primitive turns simple arithmetic into .0 field access and manual trait impls, so the pattern earns its place at domain boundaries (public APIs, database IDs, money) rather than everywhere a u64 appears.
Typestate has the sharpest cost curve.
Each additional state multiplies the number of impl blocks, and the state type parameter bleeds into every touching function signature, so it is best reserved for protocols where an illegal call sequence would be a real incident, such as connection handshakes, not for UI toggle state.
Builder complexity scales differently: it is cheap to add but easy to over-apply to a three- or four-field struct, where a plain struct literal or a Default derive would be just as safe and more readable.
RAII's failure modes are subtler because they surface only under specific runtime conditions.
Cleanup logic that assumes unwinding silently does not run under panic = "abort", and a Drop implementation that itself panics during another panic aborts the process instead of merely leaking.
Architecturally, these patterns cluster: a typestate connection lifecycle is usually built through a builder for its initial configuration, its intermediate states are often newtypes or marker structs, and the resources it holds are released through RAII guards.
That clustering is a useful review lens: hand-rolled state enums with scattered "invalid transition" runtime checks are a sign typestate would move those checks to compile time, and cleanup duplicated across every early-return branch is a sign a scope guard would collapse it into one place.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Newtype | Zero-cost nominal typing, centralizes validation | Wrapper boilerplate, "fatigue" if overused | Domain IDs, validated strings, money, FFI-safe wrappers |
| Builder | Readable construction of complex values, validation runs once | Adds a second type per product, easy to over-apply to small structs | Many optional fields, config objects, client construction |
| Typestate | Illegal call sequences become compile errors, no runtime state checks | Generic explosion, awkward at serialization/FFI boundaries | Protocol/connection lifecycles, multi-step transactions |
| RAII / Drop guards | Deterministic cleanup on every exit path, no GC needed | No async Drop, breaks under panic = "abort" without care | Locks, files, temp resources, timers, rollback-on-drop transactions |
Common Misconceptions
- "Rust patterns are just the GoF patterns with different syntax" - most GoF patterns exist to work around inheritance, virtual dispatch, and null; Rust's ownership model and lack of inheritance make several of them unnecessary or meaningless, while producing a handful of patterns (typestate, newtype-as-nominal-typing) that have no direct GoF analog.
- "The typestate pattern requires
unsafe" - it is built entirely from ordinary generics, marker structs, andPhantomData, all of which are safe Rust;unsafeis not part of the pattern itself. - "RAII is just the
Droptrait" -Dropis the mechanism, but RAII is the broader discipline of tying every resource's lifetime to a value's ownership scope, which also involves designing constructors that always leave a value in a valid, cleanup-ready state. - "A newtype is just a wrapper struct, nothing special" - the significance is nominal typing enforced by the compiler at zero runtime cost, which is a stronger and cheaper guarantee than a comment or a naming convention can provide.
- "Builder pattern always needs a derive macro or external crate" - a hand-written builder with a handful of chained
self-consuming setters is often clearer than a generated one, and crates liketyped-builderorbonare conveniences for large structs, not a requirement of the pattern.
FAQs
What makes a pattern "idiomatic Rust" versus a pattern borrowed from another language?
An idiomatic Rust pattern leans on ownership, move semantics, and the trait system to enforce a guarantee at compile time, rather than relying on inheritance, runtime type checks, or defensive null handling that Rust's type system makes unnecessary.
Why doesn't Rust have classic inheritance-based patterns like Template Method or Decorator in their original form?
Rust has no implementation inheritance between structs, only trait-based shared behavior and composition, so patterns built around overriding a parent class's method are typically reworked as trait default methods, generics, or explicit composition instead.
How does the newtype pattern actually prevent bugs at compile time?
It creates a distinct nominal type for the compiler to check, so a function expecting a UserId will reject an OrderId argument even though both wrap a u64, and this check happens during type checking, before the program ever runs.
How does the typestate pattern turn a runtime error into a compile error?
By encoding state as a type parameter, methods only valid in one state are only implemented on that state's type, so calling .query() on Connection<Disconnected> is not a method that exists to call, and the compiler reports "no method found" instead of the program panicking or misbehaving at runtime.
Does RAII replace the need for a garbage collector in Rust?
For resource cleanup, yes: RAII deterministically releases memory, locks, files, and handles the moment their owning value goes out of scope, which is why Rust does not need a garbage collector for correctness, though it is a different mechanism than a GC and has different failure modes (like non-async Drop).
When is a plain struct literal better than the builder pattern?
When a type has a small, mostly-required set of fields (roughly three or fewer) and no meaningful validation step, a struct literal or a Default derive with a couple of overrides is simpler to read and maintain than a separate builder type.
Can the newtype and typestate patterns be combined?
Yes, and it is common: typestate markers are frequently zero-sized newtype-like structs (struct Open;, struct Closed;) used purely as type parameters, so the two patterns share the same underlying nominal-typing mechanism.
What is the biggest risk of overusing the typestate pattern?
Generic parameter explosion: each additional state multiplies the number of impl blocks needed, and the state type parameter tends to leak into every function signature that touches the value, which can make the API harder to read than the runtime check it replaced.
Why can't `Drop` be `async`?
Drop::drop is a synchronous method with no way to yield to an executor, so cleanup that genuinely requires awaiting an operation (like an async network close) cannot happen automatically when a value is dropped and must be triggered explicitly before the value goes out of scope.
Is "zero-cost abstraction" really zero cost for these patterns?
For newtype and typestate, yes in the common case: both are erased entirely at compile time, so the compiled code is identical to writing the unwrapped, unchecked version by hand; RAII has the ordinary cost of the Drop call itself, which is the same cost as writing that cleanup call manually.
What's the difference between a type alias and the newtype pattern?
A type alias (type UserId = u64;) is just another name for the exact same type, so a UserId and a raw u64 are freely interchangeable; a newtype (struct UserId(u64);) creates a genuinely distinct type that the compiler will not silently interchange with u64.
How do I decide which of these four patterns, if any, a piece of code actually needs?
Ask what bug the pattern would prevent and how often that bug class actually recurs in the codebase: identifier mix-ups point to newtype, half-configured objects point to builder, illegal call ordering points to typestate, and leaked or double-released resources point to RAII, and if none of those bugs are a recurring problem, a plainer approach is usually the better fit.
Related
- Rust Patterns Basics - the hands-on walkthrough this page anchors, with ten runnable examples across the section.
- The Newtype Pattern - a full recipe-style deep dive into nominal typing with wrapper structs.
- Builder Pattern - construction ergonomics for structs with many optional fields.
- Typestate Pattern - encoding state machines in the type system in detail.
- RAII & Drop Guards - scope-tied resource cleanup and guard types.
- Ownership Basics - the ownership and move-semantics foundation every pattern on this page builds on.
Stack versions: This page is conceptual and not tied to a specific stack version, though illustrative snippets follow Rust 1.97.0 (edition 2024) conventions used throughout this section.