Why doesn't Rust have inheritance? A design perspective
Explore why why doesn't rust have inheritance, how Rust achieves polymorphism with traits, and practical patterns for code reuse through composition and generics. This educational guide covers design decisions, performance considerations, and API design tips for Rust developers.

Rust does not use classical inheritance by design. Instead, Rust emphasizes composition and trait-based polymorphism to achieve code reuse and safety. This approach avoids the complexity and runtime costs of inheritance, while enabling flexible APIs through trait bounds, generics, and optional dynamic dispatch. In this article we unpack the reasoning and practical patterns you can apply in real projects.
Why Rust avoids classical inheritance
In many object-oriented languages, inheritance provides a shared base class and a runtime dispatch path. Rust does not implement class-based inheritance; instead, it relies on composition and traits to define behavior. This choice reduces runtime cost, avoids the diamond problem, and strengthens module boundaries. Practically, you define small, reusable types and implement traits on them, then compose behavior by combining these pieces. The phrasing why doesn't rust have inheritance comes up frequently for new Rustaceans; the language design deliberately limits inheritance to preserve safety and clarity. The contrast can be seen in the absence of a base class hierarchy and the emphasis on explicit interfaces.
struct Cat { name: String }
impl Cat { fn speak(&self) { println!("meow"); } }
struct Dog { name: String }
impl Dog { fn speak(&self) { println!("bark"); } }- In this simple example, Cat and Dog do not share a common base type providing speak; instead, you model behavior through separate impl blocks.
- This avoids shared mutable state pitfalls and makes behavior explicit per type.
Why this matters: avoiding inheritance reduces runtime costs and prevents brittle hierarchies, enabling safer composition of capabilities.
trait Speak { fn speak(&self); }
struct Cat2 { name: String }
impl Speak for Cat2 { fn speak(&self) { println!("meow"); } }
struct Dog2 { name: String }
impl Speak for Dog2 { fn speak(&self) { println!("woof"); } }- Define a minimal interface, then implement it for concrete types. This is the core of Rust’s approach to polymorphism.
lengthy_code_note_for_section_1_continue
Steps
Estimated time: 60-90 minutes
- 1
Define a shared behavior as a trait
Create a trait that captures the behavior you want to share across types, rather than a base class. This defines an interface without forcing a single inheritance chain.
Tip: Keep traits focused and small to maximize reuse and composability. - 2
Implement the trait on concrete types
Provide concrete behavior for each type by implementing the trait. This replaces a shared base class with explicit implementations.
Tip: Use explicit impl blocks to keep behavior localized to each type. - 3
Use generics for compile-time polymorphism
Write functions that accept any type implementing the trait, enabling zero-cost abstractions through monomorphization.
Tip: Prefer generic bounds to avoid dynamic dispatch when possible. - 4
When dynamic dispatch is needed
If you need runtime flexibility, use trait objects with Box<dyn Trait> or &dyn Trait.
Tip: Be mindful of the costs of dynamic dispatch and lifetimes. - 5
Compose behavior through struct composition
Assemble complex types by combining smaller components (structs) and delegating behavior via trait implementations.
Tip: Composition often scales better than deep inheritance trees.
Prerequisites
Required
- Required
- Required
- Familiarity with basic Rust concepts (traits, generics)Required
- Command-line terminal accessRequired
Optional
- Optional
Commands
| Action | Command |
|---|---|
| Check Rust versionVerify installed toolchain version | rustc --version |
| Create a new binary projectSet up a fresh project for experiments | cargo new my_app --bin |
| Build the projectCompile to ensure code correctness | cargo build |
| Run the binaryExecute the compiled binary in the current project | cargo run |
| Run testsRun unit/integration tests | cargo test |
Quick Answers
What is inheritance and why is it common in OO languages?
Inheritance is a mechanism where a new type derives behavior from an existing type. It creates a class hierarchy and enables polymorphism through base class references. Rust forgoes this model to avoid runtime costs and ambiguity in complex hierarchies.
Inheritance is when a type inherits behavior from another; Rust avoids this in favor of traits and composition for safer, clearer APIs.
What are traits and how do they replace inheritance in Rust?
Traits define a set of methods a type must implement. They provide a contract similar to an interface and enable polymorphism without a class hierarchy. Types implement traits to gain shared behavior, and functions can be generic over any type implementing a trait.
Traits are like interfaces. They let you share behavior across types without inheritance, enabling flexible and safe polymorphism.
Can Rust still express common inheritance patterns?
Rust replaces inheritance with composition and trait-based design. You can simulate common patterns by combining smaller components and using generic constraints, but you won’t have a single base class. This leads to more explicit API design and often better compile-time checks.
You can get similar outcomes using traits and composition, but Rust won’t provide a traditional base class.
When should I use trait objects vs generics?
Use generics when you want zero-cost abstractions and monomorphization. Use trait objects when you need runtime polymorphism or heterogeneous collections. Each has trade-offs in performance and flexibility.
Choose generics for performance and trait objects for runtime flexibility.
Are there performance costs to not using inheritance?
Not using inheritance avoids vtables and dynamic dispatch costs in many cases, especially with monomorphized generic code. When you need dynamic dispatch, trait objects incur a small runtime cost.
Performance can be excellent with generics; dynamic dispatch adds a small cost when used.
What are practical patterns for API design without inheritance?
Design APIs around small, composable traits, and use generic bounds to share behavior. Favor explicit interfaces and delegation to built components rather than deep hierarchies.
Build APIs with small traits and clear composition for reusable code.
Quick Summary
- Use traits for shared interfaces
- Favor composition over inheritance
- Leverage generics for zero-cost abstraction
- Reserve dynamic dispatch for runtime flexibility