All articles
Rust’s trait system is one of its most powerful features, enabling zero-cost abstractions and…
Traits & Generics

Rust’s trait system is one of its most powerful features, enabling zero-cost abstractions and…

This article assumes familiarity with Rust traits, generics, and trait bounds. We’ll explore the pyramid pattern through three complete…

By Luis SoaresJanuary 12, 2026Original on Medium

Rust’s trait system is one of its most powerful features, enabling zero-cost abstractions and compile-time polymorphism. But as codebases grow, a common question emerges: how do you organize traits that build upon each other? One effective pattern is the abstraction pyramid — a layered hierarchy where each level adds specific capabilities while preserving access to everything below.

This article assumes familiarity with Rust traits, generics, and trait bounds. We’ll explore the pyramid pattern through three complete, runnable examples that you can clone and experiment with.

The Pyramid Structure

An abstraction pyramid organizes traits from general to specific:

Specialized Traits (Peak)
                 /        \
        Domain Traits    (Middle)
              /    \
     Foundation Traits   (Base)

Foundation traits define the broadest capabilities — operations that apply across many types. Domain traits inherit from the foundation and add context-specific behavior. Specialized traits sit at the peak, providing the most specific functionality for particular use cases.

The key principle: each layer adds exactly one coherent capability. This maintains the Liskov Substitution Principle — any type implementing a higher-level trait can be used wherever a lower-level trait is expected.

Consider why this matters. When you write fn process<T: SpecializedTrait>(item: &T), you can call methods from all three layers. But when you write fn store<T: FoundationTrait>(item: &T), you accept any type in the pyramid, maximizing code reuse. The pyramid gives API consumers flexibility to work at their preferred abstraction level.

Example 1: Storage System

Let’s build a storage abstraction that progresses from basic key-value operations to encrypted persistent storage.

// === FOUNDATION: Basic storage operations ===
pub trait Storage {
    type Error;
    fn get(&self, key: &str) -> Result<Option<Vec<u8>>, Self::Error>;
    fn set(&mut self, key: &str, value: &[u8]) -> Result<(), Self::Error>;
    fn delete(&mut self, key: &str) -> Result<bool, Self::Error>;
    fn exists(&self, key: &str) -> Result<bool, Self::Error>;
}

// === DOMAIN: Persistent storage with durability guarantees ===
pub trait PersistentStorage: Storage {
    fn flush(&mut self) -> Result<(), Self::Error>;
    fn sync(&self) -> Result<(), Self::Error>;
}

// === SPECIALIZED: Encrypted storage with key management ===
pub trait EncryptedStorage: PersistentStorage {
    fn rotate_key(&mut self) -> Result<(), Self::Error>;
    fn encrypted_keys(&self) -> Vec<String>;
}

Each layer adds one concern: basic CRUD, durability, then security. Here’s a concrete implementation:

pub struct MemoryStorage {
    data: HashMap<String, Vec<u8>>,
}

impl Storage for MemoryStorage {
    type Error = StorageError;

    fn get(&self, key: &str) -> Result<Option<Vec<u8>>, Self::Error> {
        Ok(self.data.get(key).cloned())
    }

    fn set(&mut self, key: &str, value: &[u8]) -> Result<(), Self::Error> {
        self.data.insert(key.to_string(), value.to_vec());
        Ok(())
    }

    fn delete(&mut self, key: &str) -> Result<bool, Self::Error> {
        Ok(self.data.remove(key).is_some())
    }

    fn exists(&self, key: &str) -> Result<bool, Self::Error> {
        Ok(self.data.contains_key(key))
    }
}

Now functions can accept different abstraction levels:

// Works with ANY storage
fn count_keys<S: Storage>(storage: &S, prefix: &str) -> usize {
    // Foundation-level operation
}

// Requires durability guarantees
fn backup<S: PersistentStorage>(storage: &mut S) -> Result<(), S::Error> {
    storage.flush()?;
    storage.sync()
}

// Only for encrypted backends
fn security_audit<S: EncryptedStorage>(storage: &S) -> Vec<String> {
    storage.encrypted_keys()
}

The pyramid enables writing generic code at the appropriate level — neither over-constrained nor under-specified.

Example 2: HTTP Client

HTTP clients naturally form a pyramid: basic requests → authenticated requests → resilient requests with retry logic.

// === FOUNDATION: Basic HTTP operations ===
pub trait HttpClient {
    fn get(&self, url: &str) -> Result<Response, HttpError>;
    fn post(&self, url: &str, body: &[u8]) -> Result<Response, HttpError>;
    fn request(&self, req: Request) -> Result<Response, HttpError>;
}

// Notice how AuthenticatedClient provides a default implementation of get_authenticated that uses the foundation's request method. Higher layers can leverage lower layers' capabilities while adding their own.

A concrete implementation must satisfy all three levels:

pub struct ApiClient {
    base_url: String,
    token: Option<String>,
    max_retries: u32,
}

impl HttpClient for ApiClient {
    fn get(&self, url: &str) -> Result<Response, HttpError> {
        self.request(Request::get(url))
    }
    fn post(&self, url: &str, body: &[u8]) -> Result<Response, HttpError> {
        self.request(Request::post(url).body(body))
    }
    fn request(&self, req: Request) -> Result<Response, HttpError> {
        // Actual HTTP implementation
        Ok(Response::new(200, b"OK".to_vec()))
    }
}

impl AuthenticatedClient for ApiClient {
    fn auth_header(&self) -> Option<String> {
        self.token.as_ref().map(|t| format!("Bearer {}", t))
    }
    fn refresh_token(&mut self) -> Result<(), HttpError> {
        self.token = Some("refreshed_token".to_string());
        Ok(())
    }
}

impl RetryingClient for ApiClient {
    fn max_retries(&self) -> u32 { self.max_retries }
    fn backoff_ms(&self, attempt: u32) -> u64 { 100 * 2_u64.pow(attempt) }
}

Example 3: Game Entity System

Game engines often need multiple inheritance paths. An entity might be both renderable and physical. Let’s model this with intersecting pyramids:

// === FOUNDATION ===
pub trait Entity {
    fn id(&self) -> u64;
    fn position(&self) -> Vec2;
    fn update(&mut self, delta: f32);
}

// === DOMAIN: Two parallel branches ===
pub trait Renderable: Entity {
    fn sprite(&self) -> &str;
    fn layer(&self) -> i32;
    fn visible(&self) -> bool;
}

Practice what you learned

Reinforce this article with hands-on coding exercises and AI-powered feedback.

View all exercises

pub trait Physical: Entity { fn velocity(&self) -> Vec2; fn mass(&self) -> f32; fn apply_force(&mut self, force: Vec2); }

// === SPECIALIZED: Combines both branches === pub trait Character: Renderable + Physical { fn health(&self) -> i32; fn take_damage(&mut self, amount: i32); }


This creates a diamond pattern where `Character` inherits from two domain traits that share the same foundation. Rust handles this elegantly—there's only one `Entity` implementation, accessible through either path.

```rust
pub struct Player {
    id: u64,
    pos: Vec2,
    vel: Vec2,
    health: i32,
}

impl Entity for Player {
    fn id(&self) -> u64 { self.id }
    fn position(&self) -> Vec2 { self.pos }
    fn update(&mut self, delta: f32) {
        self.pos.x += self.vel.x * delta;
        self.pos.y += self.vel.y * delta;
    }
}

impl Renderable for Player {
    fn sprite(&self) -> &str { "player.png" }
    fn layer(&self) -> i32 { 10 }
    fn visible(&self) -> bool { true }
}

impl Physical for Player {
    fn velocity(&self) -> Vec2 { self.vel }
    fn mass(&self) -> f32 { 80.0 }
    fn apply_force(&mut self, force: Vec2) {
        self.vel.x += force.x / self.mass();
        self.vel.y += force.y / self.mass();
    }
}

impl Character for Player {
    fn health(&self) -> i32 { self.health }
    fn take_damage(&mut self, amount: i32) { self.health -= amount; }
}

Now your game systems can operate at appropriate abstraction levels:

fn render_all(entities: &[&dyn Renderable]) { /* ... */ }
fn physics_step(entities: &mut [&mut dyn Physical], dt: f32) { /* ... */ }
fn process_combat(characters: &mut [&mut dyn Character]) { /* ... */ }

Advanced Patterns

Blanket Implementations

Blanket implementations let you provide automatic trait implementations for any type satisfying certain bounds:

// Every EncryptedStorage automatically gets this capability
impl<T: EncryptedStorage> Auditable for T {
    fn audit_log(&self) -> Vec<AuditEntry> {
        self.encrypted_keys()
            .into_iter()
            .map(|k| AuditEntry::new(&k))
            .collect()
    }
}

Sealed Traits

When you want a pyramid that users can use but not extend, seal the foundation:

mod private {
    pub trait Sealed {}
}

pub trait DatabaseDriver: private::Sealed {
    fn execute(&self, query: &str) -> Result<Rows, Error>;
}

// Only types in this crate can implement Sealed,
// therefore only this crate can add new drivers
impl private::Sealed for PostgresDriver {}
impl DatabaseDriver for PostgresDriver { /* ... */ }

Marker Traits for Compile-Time Guarantees

Empty traits can encode capabilities without runtime cost:

pub trait ThreadSafe: Storage {}
pub trait TransactionSupport: Storage {}

fn parallel_write<S: Storage + ThreadSafe>(storage: &S) { /* ... */ }
fn atomic_batch<S: Storage + TransactionSupport>(storage: &S) { /* ... */ }

When NOT to Use Pyramids

Pyramids aren’t always the answer. Avoid them when:

The domain is simple. If you have three traits that don’t build on each other, don’t force a hierarchy. Flat traits composed with + bounds work better:

// Prefer this for unrelated capabilities
fn process<T: Serialize + Validate + Transform>(item: T) { }

// Over artificial hierarchies
trait Processable: Serialize + Validate + Transform {}  // Unnecessary

Composition fits better. When types need some capabilities but not others, composition beats inheritance:

struct Entity {
    renderer: Option<Box<dyn Renderable>>,
    physics: Option<Box<dyn Physical>>,
    ai: Option<Box<dyn Thinking>>,
}

The hierarchy exceeds 3–4 levels. Deep pyramids become hard to understand and maintain. If you’re adding a fifth level, reconsider the design.

Signs of over-engineering: You have traits with only one implementor. You’re adding layers “for future flexibility.” Types implement many traits but only use methods from one.

Conclusion

Abstraction pyramids help organize complex trait hierarchies by building from general to specific. Each layer adds one coherent capability, enabling code reuse through polymorphism at multiple levels.

The pattern works best for domains with clear “is-a” relationships: storage systems, protocol handlers, entity hierarchies. It shines in library design where users need flexibility in how they interact with your API.

Start flat. Extract hierarchies only when you see repeated patterns and genuine need. The right abstraction emerges from use, not speculation.

The complete, runnable examples from this article are available at github.com/luishsr/trait-pyramid-examples. Clone it, run cargo run, and experiment with extending the pyramids yourself.

All code examples compile with Rust 1.70+. Full source: github.com/luishsr/trait-pyramid-examples

Practice what you learned

Reinforce this article with hands-on coding exercises and AI-powered feedback.

View all exercises

Want to practice Rust hands-on?

Go beyond reading — solve interactive exercises with AI-powered code review on Rust Lab.