Structs & Enums Best Practices
Design data types so invariants live in one place and the compiler helps enforce them.
How to Use This List
- Apply when designing new domain types before writing logic.
- Review during PRs that add
boolflags orOptionsoup. - Pair with Domain Modeling with Types.
A - Struct Design
- Prefer small focused structs over god objects. Split when fields group into distinct lifecycles.
- Make fields private; expose constructors and accessors. Preserve invariants.
- Use newtypes for IDs and units.
struct UserId(u64)prevents argument swaps. - Derive
Debug,Clone,Eqwhen semantics match. Document when not deriving. - Avoid public
Vecfields unless truly unrestricted. Expose iterators or methods.
B - Enum Design
- Replace boolean pairs with enums when states multiply.
enum Phasebeatsis_started+is_done. - Put data only on variants that need it.
ReadyvsRunning { token }. - Use
#[non_exhaustive]on extensible public enums. Forward-compatible evolution. - Transition via methods consuming
self. Illegal transitions become type orResulterrors. - Box large variant payloads. Keep enum size reasonable.
C - Pattern Matching
- Keep
matcharms small; extract helpers. Readable transition functions. - Prefer
if letfor single-pattern flows. Reduce noise vs one-armmatch. - Use
let elsefor required patterns. Early return on unexpected shape. - Avoid
unwrapon domain enums in library code. ReturnResultwith context. - Test every variant in critical paths. Variant addition should fail tests until updated.
D - Serialization and APIs
- Separate wire DTOs from domain types when shapes differ. Validate at boundary.
- Use serde attributes on enums for tagged JSON.
#[serde(tag = "type")]etc. - Document variant semantics in rustdoc. Consumers match externally too.
- Version persisted enum discriminants. Migration plan when reordering variants.
- Do not leak internal enums in public API without stability story. Semver implications.
FAQs
Struct vs enum for optional fields?
Many optional combos suggest enum variants or sub-structs.When tuple struct?
Single-field newtypes and thin wrappers.Builder pattern?
Many optional fields at construction - separate builder struct.Flattened enums in DB?
Store tag + JSON payload; validate on read.Too many variants?
Split into sub-enums or trait objects at boundary.Clone vs reference in variants?
Own data in variants; borrow in methods with lifetimes if zero-copy.Default variant?
`#[default]` on enum for `Default` derive.Exhaustive tests?
`match` in test over all variants with compile-time check.Graph nodes?
Indices into `Vec` rather than `Rc<RefCell<Node>>` until needed.Next steps?
Traits for behavior on these types.Related
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.